SciELO - Scientific Electronic Library Online

 
vol.27 número1The Subalgebra Lattice of A Finite Diagonal–Free Two–Dimensional Cylindric Algebra índice de autoresíndice de materiabúsqueda de artículos
Home Pagelista alfabética de revistas  

Servicios Personalizados

Revista

Articulo

Indicadores

Links relacionados

  • No hay artículos similaresSimilares en SciELO

Compartir


Computación y Sistemas

versión On-line ISSN 2007-9737versión impresa ISSN 1405-5546

Comp. y Sist. vol.27 no.1 Ciudad de México ene./mar. 2023  Epub 16-Jun-2023

https://doi.org/10.13053/cys-27-1-4529 

Articles of the thematic section

Specifying and Verifying a Transformation of Recursive Functions into Tail-Recursive Functions

Axel Suárez Polo1 

José de Jesús Lavalle Martínez1  * 

Iván Molina Rebolledo1 

11 Benemérita Universidad Autónoma de Puebla, Puebla, Mexico. axel.suarez@alumno.buap.mx, gustavo.molinar@alumno.buap.mx.


Abstract:

It is well known that some recursive functions admit a tail recursive counterpart which have a more efficient time-complexity behavior. This paper presents a formal specification and verification of such process. A monoid is used to generate a recursive function and its tail-recursive counterpart. Also, the monoid properties are used to prove extensional equality of both functions. In order to achieve this goal, the Agda programming language and proof assistant is used to generate a parametrized module with a monoid, via dependent types. This technique is exemplified with the length, reverse, and indices functions over lists.

Keywords: Dependent types; formal specification and verification; tail recursion; accumulation; program transformation

1 Introduction

Dependently typed programming languages provide an expressive system that allows both programming and theorem proving. Agda is an implementation of such a kind of language [7].

Using these programming languages, it can be proved that two functions return the same output when they receive the same input, which is a property known as extensional equality [6].

In Agda, the totality of a function is a requirement of the language, which means that every function must always return a value for any input, while ensuring that the function always terminates; which gives us a proof of termination for any construct within Agda [7].

Agda was chosen because of our familiarity with it, nevertheless there are other alternatives such as Coq [3] or Lean [8]. Programs can be developed using a transformational approach, where an initial program whose correctness is easy to verify is written, and after that, it is transformed into a more efficient program that preserves the same properties and semantics [14].

Proving that the transformed program works the same way as the original program is usually done by using algebraic reasoning [4], but this can also be done using dependently typed programming [13], with the advantage of the proof being verified by the compiler.

The accumulation strategy is a well-known program transformation technique to improve the efficiency of recursive functions [5].

This technique is the focus of this paper, in which dependently typed programming is used to develop a strategy to prove extensional equality between the original recursive programs and their tail-recursive counterparts. The source code of this paper is available here.

2 A Simple Example: List Length

Let us start with a simple example: a function to compute the length of a list. This function can be defined recursively as follows:

len:ListAlen[]=0len(xxs)=suc(lenxs)

Nonetheless, this function requires space proportional to the length of the list due to the recursive calls. This program can be transformed into a tail-recursive function, which can be optimized automatically by the compiler to use constant space [2]. The transformed function is shown below:

len-tl:ListAlen-tl[]n=nlen-tl(xxs)n=len-tlxs(sucn)

In this example, it is clear to see that both functions return the same result for every possible list we provide as input. This fact can be represented in Agda using dependent function types:

len=len-tl:(xs:ListA)lenxslen-tlxs0

The notion of “sameness” used here is the one of intensional equality, which is an inductively defined family of types [9, 13] with the following definition:

data__{a}{A:Seta}(x:A):ASetawhereinstancerefl:xx

This means that two terms are equal if they are exactly the same term. Additionally, in Agda, if both terms reduce to the same term, we can state that they are intensionally equal. For example:

refl:2+35

This notion of equality together with the addition of the universal quantifier, allows us to state a kind of equality for functions, known as point-wise equality or extensional equality [6].

To prove extensional equality for the length functions, we can proceed inductively over the list, which has the [] and xxs cases:

lenlen-tl:(xs:ListA)lenxslen-tlxs0lenlen-tl[]=?lenlen-tl(xxs)=?

The base case is trivial, because both sides of the equality in the resulting type reduce to the same term, which is:

len[]=0(bydefinition)len-tl[]0=0

Therefore, we can fill the first hole in our proof with the refl constructor, such that the resulting equation is:

lenlen-tl[]=refl

For the inductive case, we can reduce both sides of the equality instantiated with the argument, and check what is necessary to prove.

Note that this can be done automatically by querying Agda, and it is particularly useful when using the Agda mode in Emacs [17]. The reductions are shown below and follow from the definition:

len(xxs)=suc(lenxs)len-tl(xxs)0=len-tlxs(suc0)=len-tlxs1

We need to prove that suc(lenxs)len-tlxs1. This time, we cannot simply use refl, because both sides do not reduce to the same term. For this reason, we can proceed to call this function recursively with the tail of the list.

This is justified because of the Curry-Howard correspondence, and the fact that we are making a proof by induction. The result of the recursive call gives us the induction hypothesis:

lenlen-tl(xxs)=letind-h=lenlen-tlxsin?

The type of indh is lenxslen-tlxs0. The left sides of the induction hypothesis and what we are proving are almost the same. To make them match, we can apply the congruence property of equality, which has the following type:

cong:(f;AB){xy}xyfxfy

Applying this function to the induction hypothesis, we get the function below:

lenlen-tl(xxs)=letind-h=lenlen-tlxssuc-cong=congsucind-hin?

The suc-cong term has the type:

suc(lenxs)suc(len-tlxs0)

As we can see the left sides match, so we can change our goal to prove that the right side of succong is equal to the right side of the goal; by making use of the transitive property of equality, which has the following type in Agda:

trans:{xyz}xyyzxz

Therefore, now our proof is:

lenlen-tl(xxs)=letind-h=lenlen-tlxssuc-cong=congsucind-hintranssuc-cong?

The type of the term required to fill the hole is:

suc(len-tlxs0)len-tlxs1

We need to “pull” the 1 from the accumulator somehow, and convert it to a suc call. We can extract this new goal into a helper function:

len-pull:(xs:ListA)suc(len-tlxs0)=len-tlxs1

We can try to prove this goal by straightforward induction over the list, but we reach a dead end:

len-pull[]=refllen-pull(xxs)=?

The base case is trivial, following the definitions of the function, both terms reduce to 1. The problem is the inductive case, which reduces as follows:

suc(len-tl(xxs)0)=suc(len-tlxs(suc0))=suc(len-tlxs1)suc(len-tl(xxs)1)=len-tlxs(suc1)=len-tlxs2

So, we are left with the following goal, which is very similar to the one we started with:

suc(len-tlxs1)len-tlxs2

We could try to prove this proposition by straightforward induction too, but that would require to prove a similar proposition for the next values 2 and 3, and so on.

To solve this issue, we can use a generalization strategy to prove this inductive property [1]. The generalized property will allow us to vary the value of the accumulator in the different cases of the inductive proof, but we will need to introduce another variable for it.

It is important to note that after processing the first n items of the list, we will get n+len-tlxs0 on the left side and len-tlxsn on the right one. Combining the generalization strategy and this fact, we can see that the property we have to prove is:

len-pull-generalized:(xs:ListA)(np:)n+len-tlxsplen-tlxs(n+p)

This function can be proved by induction over the list:

len-pull-generalized[]np=refllen-pull-generalized(xxs)np=?

The base case is trivial, because replacing the xs argument with [], and following a single reduction step on both sides, the common term n+p is reached. The inductive case is more interesting. Reducing both sides of the equation proceeds as follows:

n+len-tl(xxs)p=n+len-tlxs(sucp)len-tl(xxs)(n+p)=len-tlxs(suc(n+p))n+len-tl(xxs)p=n+len-tlxs(sucp)

We can see that we have pretty much the induction hypothesis, with the only difference being the accumulating parameter p. Nevertheless, as we have generalized the proposition, we can pick a value for p when using the induction hypothesis:

len-pull-generalized(xxs)np=len-pull-generalizedxsn(sucp)

This takes us closer to the goal we want to prove. Unfortunately, we are left with the following goal after performing the substitution of p with sucp:

n+len-tlxs(sucp)len-tlxs(n+sucp)

This is almost what we want, except for suc(n+p) not being equal to n+sucp. However, these two terms are indeed equal, but not definitionally, because the plus function is defined by induction on the first argument, and not on the second one:

_+_:NatNatNatzero+m=msucn+m=m

Therefore, applying reduction steps does not allow Agda to deduce the equality of these two terms. Fortunately, the fact that these terms are equal can be easily proved inductively as follows:

+-suc:mnm+sucnsuc(m+n)+-suczeron=refl+-suc(sucm)n=congsuc(+-sucmn)

The remaining step is to “replace” the suc(n+p) term with n+sucp. Agda provides the rewrite construct to perform this transformation:

len-pull-generalized(xxs)nprewrite(sym(+-sucnp))=len-pull-generalizedxsn(sucp)

We make use of the symmetric property of equality in the rewriting step, which allows us to flip the sides of the equality:

sym:{xy}xyyx

With all this in place, we can finally prove the remaining goals, giving as a result the complete proof:

len-pull-generalized:(xs:ListA)(np:)n+len-tlxsplen-tlxs(n+p)len-pull-generalized[]np=refllen-pull-generalized(xxs)nprewrite(sym(+-sucnp))=len-pull-generalizedxsn(sucp)len-pull:(xs:ListA)suc(len-tlxs0)len-tlxs1len-pullxs=len-pull-generalizedxs10lenlen-tl:(xs:ListA)lenxslen-tlxs0lenlen-tl[]=refllenlen-tl(xxs)=let ind-h=lenlen-tlxssuc-cong=congsucind-hsuc-pull=len-pullxsintranssuc-congsuc-pull

3 Another Example: List Reverse

The list reversal function follows a similar pattern to the one we have seen before:

reverse:ListA->ListAreverse[]=[]reverse(xxs)=reversexs++(x[])reverse-tl:ListA->ListA->ListAreverse-tl[]ys=ysreverse-tl(xl)l'=reverse-tll(x<l')

It should not come as a surprise that the equality proof is very similar too:

reverse-pull-generalized:(xsyszs:ListA)reverse-tlxsys++zsreverse-tlxs(ys++zs)reverse-pull-generalized[]yszs=reflreverse-pull-generalized(xxs)yszs=reverse-pull-generalizedxs(xys)zsreverse-pull:(x:A)(xs:ListA)reverse-tlxs[]++(x[])reverse-tlxs(x[])reverse-pullxxs=reverse-pull-generalizedxs[](x[])reversereverse-tl:(xs:ListA)reversexsreverse-tlxs[]reversereverse-tl[]=reflreversereverse-tl(xxs)=letind-h=reversereverse-tlxsappend-cong=cong(_++(x[]))ind-happend-pull=reverse-pullxxsintransappend-congappend-pull

There are minor variations in the function signatures and the order of the parameters, but the structure is identical:

– Start proving by induction on the list.

– Fill the base case with refl.

– Take the inductive hypothesis by using a recursive call.

– Apply an operator to both sides of the equality, using cong.

– Create a function to pull the accumulator, and prove it using a generalized version of this function that allows varying the accumulator.

– Compose the two equalities using the trans function.

4 Generalization

Starting from the function definitions, we can see that they follow the same recursive pattern, we can write this pattern in Agda, which is just a specialization of a fold function [11, 12]:

reduce:ListARreduce[]=emptyreduce(xxs)=fx<>reducexs

where

R is the result type of the function.

empty is the term to return when the list is empty.

f is a function to transform each element of the list into the result type.

<> is the function to combine the current item and the recursive result.

In the case of the len function, the result type is , the natural numbers; empty is 0; the function to transform each element is a constant function that ignores its argument and returns 1; and the function to combine the current item and the result of the recursive call is the addition function.

For the reverse function, the result type is the same type as the original list, ListA; empty is the empty list; the function to transform each element creates just a singleton list from its parameter; and the function to combine the current transformed item and the result of the recursive call, is the flipped concatenation function.

The flipping is necessary to make the function concatenate its first argument to the right:

reduce(xxs)=(λa®a[])x<>reducexs=(x[])<>reducexs=(λxsysys++xs)(x[])(reducexs=reducexs++(x[])

The functions that follow this pattern, can be defined in a tail-recursive way as follows:

reduce-tl:ListARRreduce-tl[]r=rreduce-tl(xxs)r=reduce-tlxs(r<>fx)

We can check manually that this function matches the tail-recursive definition in the case of the reverse function:

reverse-tl(xxs)=reduce-tlxs(r<>(λaa[])x)=reduce-tlxs(r<>(x[]))=xs((λxsysys++xs)r(x[]))=reduce-tlxs((x[]++r)=reduce-tlxs(xr)

Now we can proceed to prove that these two functions are extensionally equal in the general case. The proof follows the same pattern as the one for the len function:

reducereduce-tl:(xs:ListA)reducexsreduce-tlxsemptyreducereduce-tl[]=reflreducereduce-tl(xxs)=letind-h=reducereduce-tlxsop-cong=cong(fx<>_)ind-hop-pull=reduce-pull(fx)xsintransop-congop-pull

We make use of a piece of syntactic sugar called sections, which allows us to write the function (λrfx<>r) as (fx<>_). Apart from that, the proof is identical to the ones we have seen before.

However, to prove the accumulator pulling function, we need to use a different strategy. We are required to prove that:

reduce-pull:(r:R)(xs:ListA)r<>reduce-tlxsemptyreduce-tlxs(empty<>r)

To do this, we can prove this proposition by induction over the list, which requires us to prove the proposition when xs is []:

r<>reduce-tl[]empty=r<>emptyreduce-tl[](empty<>r)=empty<>r

So we are required to prove that r<>emptyempty<>r. We could require the <> function to be commutative, but we can “ask for less” by just requiring empty to be a left and right identity for <>, this is expressed in Agda as:

<>-identityl:(r:R)empty<>rr<>-identityr:(r:R)r<>emptyr

This way, we can use those identities to rewrite our goals, and make them match over the term r, and then, complete the base case using the trivial equality proof refl:

reduce-pullr[]rewrite<>-identitylr|<>-identityrr=refl

The inductive case goal is:

r<>reduce-tl(xxs)empty=r<>reduce-tlxs(empty<>fx)reduce-tl(xxs)(empty<>r)=reduce-tlxs((empty<>r)<>fx)

Which cannot be proved directly by straightforward induction, as we have seen before, but at least we can simplify it by using the left identity property over empty<>fx and then over empty<>r:

reduce-pullr(xxs)rewrite<>-identityl(fx)|<>-identitylr=reduce-pull-generalizedr(fx)xs

Finally, we just need to prove the generalized accumulation pulling function, which has the following type signature:

reduce-pull-generalized:(rs:R)(xs:ListA)r<>reduce-tlxssreduce-tlxs(r<>s)

Note that the base case is trivial, and it is quite similar to the ones we have already proved, so we are going to focus on the inductive case. Following the same kind of reductions we have been doing before, we can see that our goal is:

r<>reduce-tl(xxs)s=r<>reduce-tlxs(s<>fx)reduce-tl(xxs)(s<>r)=reduce-tlxs((r<>s)<>fx)

Following the generalization strategy, we have to call the function recursively, replacing the s by s<>fx, which almost gives what it is required, except that the right hand side accumulator is associated wrongly.

r<>reduce-tlxs(s<>fx)reduce-tlxs(r<>(s<>fx))

Associativity is indeed the last property that the <> function needs to satisfy. This can be expressed in Agda straightforwardly as:

<>-assoc:(rst:R)(r<>s)<>tr<>(s<>t)

Which helps us complete the proof:

reduce-pull-generalizedrs[]=reflreduce-pull-generalizedrs(xxs)rewrite<>-assocrs(fx)=reduce-pull-generalizedr(s<>fx)xs

All of these properties match the definition of a monoid. We can complete the formalization and encapsulate it in a ready to use parametrized module, using the standard library definition of a monoid:

openimportAlgebra.Structuresusing(IsMonoid)moduleGenericBasic{A:Set}{R:Set}(f:AR)(_<>_:RRR)(empty:R)(m:IsMonoid__empty)whereopenIsMonoidmusing()renaming(identitylto<>-identityl;identityrto<>-identityr;assocto<>-assoc)

5 Using the Module with the Examples

With the module in place, we can start using it to derive the recursive function, the tail-recursive counterpart, and the proof that both functions are extensionally equal.

The length function uses the usual sum monoid over the natural numbers:

openimportGenericBasic{A=}(λ_1)_+_0+-0-isMonoidrenaming(reducetolen;reduce-tltolen-tl;reducereduce-tltolenlen-tl)

The reverse function requires us to create an instance of a flipped monoid for ++, which can be done with the already defined properties for list concatenation, but flipping them when necessary.

++-flipped-isMonoid{A}=record{isSemigroup=record{isMagma=record{isEquivalence=isEquivalence;-cong=cong2(flip_++_)};assoc=λxyz®sym(++-assoczyx)};identity=++-identityr,++-identityl}

Finally, the indices function also requires us to create a custom monoid. The original indices function specialized for lists of natural number is the following:

indices:ListListindicesn[]=[]indicesn(xxs)withn=?x|yes_=0<mapsuc(indicesnxs)|no_=mapsuc(indicesnxs)

The monoid for this function has the following operation and identity element:

IndicesData:SetIndicesData=×Listempty:IndicesDataempty=0,[]_<>_:IndicesDataIndicesDataIndicesData(ln,ll)<>(rn,rl)=ln+rn,ll++map(ln+_)rl

6 Conclusions

A technique to prove extensional equality between a recursive function and its tail-recursive counterpart has been presented, along with an Agda module to automatically generate the functions and the proof from an arbitrary monoid.

As far as we know there is no related work in the literature which gives a formal proof of the extensional equality and transformation of a recursive function into a tail-recursive one, which is the main contribution of this work.

The tail-recursive function generally improves the time complexity of the original recursive function and opens the possibility of performing tail-call optimization by the compiler, leading to a more space efficient function execution [2, 15].

There are some caveats with this technique which are exemplified by the indices function.

Even though the generated function avoids mapping over the entire recursive call result, it introduces inefficiency by doing nested concatenations to the left, which leads to quadratic time complexity.

This could be solved by using higher order functions as the accumulating monoid [10], but proving the corresponding monoid laws will require to be able to transform extensional equality to intensional equality, which is not possible in Agda without using cubical type theory [6, 16], but that is out of the scope of this paper.

Further work can be done in order to generalize this result to arbitrary recursive data types and recursion schemes [12].

References

1. Abdali, S. K., Vytopil, J. (1984). Generalization heuristics for theorems related to recursively defined functions. Proceedings of the Fourth AAAI Conference on Artificial Intelligence, pp. 1–5. DOI: 10.555 5/2886937.2886938. [ Links ]

2. Bauer, A. (2003). Compilation of functional programming languages using gcc—tail calls. Master’s thesis, Institut für Informatik, Technische Universität München, Germany. [ Links ]

3. Bertot, Y., Castéran, P. (2013). Interactive theorem proving and program development: Coq’Art: the calculus of inductive constructions. DOI: 10.1007/978-3-662-07964-5. [ Links ]

4. Bird, R., De Moor, O. (1996). The algebra of programming. NATO ASI DPD, Vol. 152, pp. 167–203. [ Links ]

5. Bird, R. S. (1984). The promotion and accumulation strategies in transformational programming. ACM Transactions on Programming Languages and Systems (TOPLAS), Vol. 6, No. 4, pp. 487–504. DOI: 10.1145/1780.1781. [ Links ]

6. Botta, N., Brede, N., Jansson, P., Richter, T. (2021). Extensional equality preservation and verified generic programming. Journal of Functional Programming, Vol. 31. DOI: 10.1017/S0956796821000204. [ Links ]

7. Bove, A., Dybjer, P., Norell, U. (2009). A brief overview of agda–a functional language with dependent types. International Conference on Theorem Proving in Higher Order Logics, pp. 73–78. DOI: 10.1007/978-3-642-03359-9.pdf#page=84. [ Links ]

8. de Moura, L., Kong, S., Avigad, J., van Doorn, F., von Raumer, J. (2015). The lean theorem prover (system description). International Conference on Automated Deduction, pp. 378–388. DOI: 10.1007/978-3-319-21401-6_26. [ Links ]

9. Dybjer, P. (1994). Inductive families. Formal aspects of computing, Vol. 6, No. 4, pp. 440–465. DOI: 10.1007/BF01211308. [ Links ]

10. Hughes, J. (1986). A novel representation of lists and its application to the function “reverse”. Information Processing Letters, Vol. 22, No. 3, pp. 141–144. DOI: 10.1016/0020-0190(86)90059-1. [ Links ]

11. Hutton, G. (1999). A tutorial on the universality and expressiveness of fold. Journal of Functional Programming, Vol. 9, No. 4, pp. 355–372. DOI: 10.1017/S0956796899003500. [ Links ]

12. Meijer, E., Fokkinga, M., Paterson, R. (1991). Functional programming with bananas, lenses, envelopes and barbed wire. Conference on functional programming languages and computer architecture, pp. 124–144. [ Links ]

13. Mu, S. C., Ko, H. S., Jansson, P., . Algebra of programming using dependent types. Lecture Notes in Computer Science, pp. 268–283. DOI: 10.1007/978-3-540-70594-9_15. [ Links ]

14. Pettorossi, A., Proietti, M. (1993). Rules and strategies for program transformation. Formal Program Development, pp. 263–304. DOI: 10.1007/3-540-57499-9_23. [ Links ]

15. Rubio-Sánchez, M. (2017). Introduction to recursive programming. [ Links ]

16. Vezzosi, A., Mörtberg, A., Abel, A. (2021). Cubical agda: A dependently typed programming language with univalence and higher inductive types. Journal of Functional Programming, Vol. 31. DOI: 10.1145/3341691. [ Links ]

17. Wadler, P. (2018). Programming language foundations in agda. Brazilian Symposium on Formal Methods, pp. 56–73. DOI: 10.1007/978-3-030-03044-5_5. [ Links ]

The ? symbols are holes, which must be filled later to complete the proof, but are useful to write the proof incrementally.

Received: April 10, 2022; Accepted: June 06, 2022

* Corresponding author: José de Jesús Lavalle Martínez, e-mail: jose.lavalle@correo.buap.mx

Creative Commons License This is an open-access article distributed under the terms of the Creative Commons Attribution License