The Unfolding Semantics of Functional Programs

The idea of using unfolding as a way of computing a program semantics has been applied successfully to logic programs and has shown itself a powerful tool that provides concrete, implementable results, as its outcome is actually source code. Thus, it can be used for characterizing not-so-declarative constructs in mostly declarative languages, or for static analysis. However, unfolding-based semantics has not yet been applied to higher-order, lazy functional programs, perhaps because some functional features absent in logic programs make the correspondence between execution and unfolding not as straightforward. This work presents an unfolding semantics for higher-order, lazy functional programs and proves its adequacy with respect to a given operational semantics. Finally, we introduce some applications of our semantics.

READ FULL TEXT VIEW PDF

Authors

page 1

page 2

page 3

page 4

04/23/2018

Approximation Fixpoint Theory and the Well-Founded Semantics of Higher-Order Logic Programs

We define a novel, extensional, three-valued semantics for higher-order ...
02/06/2019

Semantics-Preserving DPO-Based Term Graph Rewriting

Term graph rewriting is important as "conceptual implementation" of the ...
11/11/2019

Recurrence Extraction for Functional Programs through Call-by-Push-Value (Extended Version)

The main way of analyzing the complexity of a program is that of extract...
12/23/2020

Representing Partial Programs with Blended Abstract Semantics

Synthesizing programs from examples requires searching over a vast, comb...
05/16/2019

Effects Without Monads: Non-determinism – Back to the Meta Language

We reflect on programming with complicated effects, recalling an undeser...
05/24/2022

Modeling Asymptotic Complexity Using ACL2

The theory of asymptotic complexity provides an approach to characterizi...
02/05/2020

A Domain Semantics for Higher-Order Recursive Processes

The polarized SILL programming language uniformly integrates functional ...
This week in AI

Get the week's most popular data science and artificial intelligence research sent straight to your inbox every Saturday.

1 Introduction

The broad field of program semantics can be classified according to the different

meanings intended to be captured or the various techniques employed. Thus, traditionally, the term denotational semantics is used when a high-level, implementation independent description of the behaviour of a program is pursued, while operational semantics usually refers to descriptions intended to capture more implementation-related properties of the execution of a program, which can then be used to gather resource-aware information, or as a blueprint for actual language implementations.

The inability of those denotational semantics to capture certain aspects of logic programs (such as the computed answer semantics) and the introduction of “impure” constructs in Prolog, led to a considerable amount of proposals for alternative semantics of logic programs during the 80’s and 90’s. One of the most remarkable proposals is the so-called s-semantics approach [4] which explores the possibility of using syntactic denotations for logic programs. In other words, programs in a very restricted form are the building blocks of the denotation, and program transformation (e.g. via unfolding) takes the role of interpretation transformers in traditional constructions. Being closer to the source code facilitates the treatment of the less declarative aspects.

However, in spite of the fact that unfolding is a technique equally applicable to functional programs, little attention has been paid to its use as a semantics tool. Investigating how unfolding can be applied to find the semantics of functional programs is the goal of this paper.

1.1 Unfolding Semantics

The process of unfolding is conceptually simple: replace any function or predicate invocation by its definition. In logic programming this amounts to unifying some literal in the body of a rule with the head of some piece of knowledge that has already been calculated, and placing the corresponding body instance where the literal was.

The previous paragraph mentions two important concepts: the first is that of piece of knowledge generated by unfolding program rules according to all current pieces of knowledge. Every piece of knowledge (called a fact) is valid source code. The set of facts may increase with every iteration. A set of facts is called an interpretation. In addition, the second concept hinted in the paragraph above is that of initial interpretation.

Prolog code / s-semantics unfolding Functional code / Funct. unfolding
add( zero,X,X).
add(suc(X),Y,suc(Z)):-add(X,Y,Z).

add Zero x = x
add (Suc x) y = Suc (add x y)

{add(zero,X,X)}
{add(zero,X,X),
add(suc(zero),X,suc(X))}

{add Zero x = x}
{add Zero x = x,
add (Suc Zero) y = (Suc y) }
Figure 1: Logic and functional versions of a simple program, and their unfoldings.

1.1.1 Unfolding in Logic Programming.

As an example, the left half of Fig. 1 shows a predicate called add that adds two Peano Naturals. This part shows the source code (upper side) together with the corresponding unfolding results (lower side).

The general unfolding procedure can be easily followed in the example, where the first two clause sets are generated ( and ).

1.1.2 Unfolding in Functional Programming.

Unfolding in functional programming (FP) follows the very same idea of unfolding in logic programming: any function invocation is replaced by the right side of any rule whose head matches the invocation.

Consider the right half of Fig. 1 as the functional version of the previous example, written in our model language. Some differences and analogies between both paradigms can be spotted: In FP, unfolding generates rules (equations) as pieces of knowledge, instead of clauses which appeared in logic programming. The starting seed is also different: bodyless rules are used in logic programming while the empty set is used in functional programming.

Finally, observe that both unfoldings (logic and functional) produce valid code and analogous results, being equivalent to . This fact provides a clue into two of the main reasons to define an unfolding semantics: first they are implementable as the procedure above shows and, second, they are also a clear point between denotational and operational semantics in proving the equivalence between a semantics of each type.

1.2 Extending Unfolding Semantics to Suit Functional Programs

Functional code Unfolding
ite :  Bool -> a -> a -> a
ite  True  t e = t
ite  False t e = e
filter:(a-> Bool)->[a]->[a]
filter p [] = []
filter p (x:xs) =
  ite (p x)
      (x:( filter p xs))
      ( filter p xs)
=
= {ite(True,t,e) = t, ite(False,t,e) = e,
filter(b,Nil) = Nil}
= {ite(True,t,e) = t, ite(False,t,e) = e,
filter(b,Nil) = Nil,
filter(b,Cons(c,Nil))|
snd(match(True,b@[c]))=Cons(c,Nil),
filter(b,Cons(c,Cons(d,e)))|
snd(match(True,b@[c]))=Cons(c,Bot),
filter(b,Cons(c,Nil))|
snd(match(False,b@[c])) = Nil
filter(b,Cons(c,Cons(d,e)))|
snd(match(False,b@[c]))=Bot}
Figure 2: Functional program requiring higher-order applications.

Section 1.1 showed that the ideas of unfolding semantics in logic programming can also be applied to FP. However, some features of FP (e.g. higher-order, laziness) render the unmodified procedures invalid.

Consider the function filter in Fig. 2. It takes a list of values and returns those values in the list that satisfy a predicate passed as its first argument.

Applying naïve unfolding to filter is impossible since ite (short for if-then-else) demands a boolean value but both p and x are unknown at unfold time (i.e. before execution).

In order to overcome this limitation, we have developed a technique capable of generating facts in the presence of incomplete information. In this case we generate conditional facts (the last four facts in ). The function match checks whether a given term matches an expression that cannot be evaluated at unfolding time (here, (p x)). Observe that match must be ready to deal with infinite values in its second argument.

Note that, in automatically-generated code, such as the unfolded code shown in Fig. 2 and the figures to come, variables are most often renamed and that our unfolding implementation uses tuples to represent curried expressions.

1.3 Related Work

One of the earliest usages of unfolding in connection to semantics is due to Scott [9], who used it to find the denotation of recursive functions, even though the word unfolding was not used at the time.

Concerning logic programming, our main inspiration source is s-semantics [4], which defines an unfolding semantics for pure logic programs that is defined as the set of literals that can be successfully derived by using the program given.

In addition, fold and unfold have been used in connection to many other problems in the field of logic programming. For example [7] describes a method to check whether a given logic program verifies a logic formula. It does this by applying program transformations that include fold and unfold.

Partial evaluation of logic programs has also been tackled by means of unfolding but it usually generates huge data structures even for simple programs.

As in logic programming, fold/unfold transformations have been used extensively to improve the efficiency of functional programs [5], but not as a way of constructing a program’s semantics.

Unfolding has also been applied to functional-logic programming [1]. However, that paper is not oriented towards finding the meaning of a program but to unfold it partially to achieve some degree of partial evaluation. Besides, it is restricted to first order, eager languages.

Paper Organization

Section 2 presents preliminary concepts. Section 3 describes the unfolding semantics itself, the core of our proposal. Section 4 presents the formal meaning we want to assign to the core language that we will be using. Section 5 points out some applications of unfolding semantics. Section 6 concludes.

2 Preliminaries

Notation

Substitutions will be denoted by . or just will denote the application of substitution to . The empty substitution will be denoted by . will denote that the expressions and have the same syntax tree.

Given a term , a position within is denoted by a dot-separated list of integers. denotes the content of position within . Replacement of the content at position within a term by some term is denoted by . The set of positions within an expression will be denoted by .

will be used to denote constructors while will denote guards.

The auxiliary functions and extract the first and second element of a tuple, respectively. Boolean conjunction and disjunction are denoted by and . (where the are terms and do not have user-defined functions) denotes its most general unifier. The conditional operator will denoted by , which has type and is defined as: , .

Regarding types, denotes a partial function from domain to domain . The type of -interpretations (sets of facts) is noted by . is intended to denote the domain from which facts are drawn. The projection of an interpretation to predefined functions only is denoted as . Lack of information is represented by in unfolding interpretations and by the well-known symbol when it is related to the minimum element of a Scott domain. Lastly, HNF stands for head normal form. An expression is said to be in head normal form if it is a variable or its root symbol is a constructor. Normal form (NF) terms are those in HNF, and whose subterms are in NF.

2.1 Core Language. Abstract Program Syntax

The language111We assume the core language to be typed although we do not develop its typing discipline here because of lack of space. that will be the base to develop this work is a functional language with guarded rules. Guards (which are optional) are boolean expressions that must be rewritten to True in order for the rule that contains it to be applicable.

Note that the language we are using is a purely functional language (meaning that it uses pattern matching,higher-order features and referential transparency).

Let us consider a signature where is the set of variables, is the set of Data Constructors that holds at least and a tuple-building constructor, holds the user-defined functions and denotes the set of predefined functions that holds at least a function match, a function nunif and a function @ that applies an expression to a list of expressions (that is, @[] represents ). and are disjoint.

Some of the sets above depend on the program under study, so they should be denoted as, e.g., but we will omit that subscript if it is clear from the context. All these sets are indexed by arity. The domains for a program are:

Terms are built with variables and constructors only. Expressions comprise terms and those constructs that include function symbols (predefined or not).

Note that the description corresponding to expressions does not allow for an expression to be applied to another expression but we still want our language to be a higher order one. We manage higher order by means of partial applications, written by using the predefined function @. Thus, un application like (where and are arbitrary expressions) is represented in our setting by @ (or by @(,) in prefix form).

To ensure that programs defined this way constitute a confluent rewriting system, these restrictions will be imposed on rules [6]: linear rule patterns, no free variables in rules (a free variable is one that appears in the rule body but not in the guard or the pattern) and finally, no superposition among rules (i.e. given a function application, at most a rule must be applicable).

The core language does not include local declarations (i.e. let, where) but this does not remove any power from the language since local declarations can be translated into aditional rules by means of lambda lifting.

3 Unfolding Semantics for the Core Language

3.1 Interpretations

Definition 1 (Fact and -Interpretation)

We will use the word fact to denote any piece of proven knowledge that can be extracted from a program and which conforms to the following restrictions: (i) They have shape , (ii) and include no symbols belonging to , (iii) Predefined functions are not allowed inside or unless the subexpression headed by a symbol in cannot be evaluated further (e.g.  would be allowed in or but would not, should be used instead) and (iv) The value of can be made equal to True (by giving appropriate values to its variables). The type of facts is denoted . Facts can be seen as rules with a restricted shape.

In addition, a -interpretation is any set of valid facts that can be generated by using the signature of a given program . The concept of -interpretation has been adapted from the concept with the same name in s-semantics.

The reason for imposing these restrictions on facts is to have some kind of canonical form for interpretations. Even with this restrictions, a program does not have a unique interpretation, but we intend to be as close to a canonical form for interpretations as possible.

3.2 Defining the Unfolding Operator

The process we are about to describe is represented in pictorial form in the Appendix, Sect. 0.A in order to help understand the process as a whole.

The unfolding operator relies on a number of auxiliary functions that are described next, together with the operator itself. A full example aimed at clarifying how these functions work can be found in the Appendix (Example 3).

3.2.1 Evaluation of Predefined Functions

=
=
=
= if can be evaluated to NF
without error. It is left untouched otherwise.
.
=
if .
=
= .
=
Figure 3: Evaluation of predefined functions

The function eval (Fig. 3) is in charge of finding a value for those expressions which do not contain any full application of user-defined functions. Since predefined functions do not have rules, their appearances cannot be rewritten, just evaluated. Only predefined functions are evaluated; all the other expressions are left untouched. Note that requires the interpretation in order to know how to evaluate predefined functions.

3.2.2 Housekeeping the Fact Set

Every time a step of unfolding takes place, new facts might be added to the interpretation. These new facts may overlap with some existing facts (that is, be applicable to the same expressions as the existing ones). Although overlapping facts do not alter the meaning of the program, they are redundant and removing them makes the interpretation smaller and more efficient. The function clean removes those redundancies. We believe this cleaning step is a novel contribution in the field of unfolding semantics for functional languages (see [2], where a semantics for logic functional programs is presented but where interpretations are not treated to remove any possible overlapping).

Given an interpretation, the function removes the overlapping pairs in order to erase redundant facts. Before defining clean, some definitions are needed.

Definition 2 (Overlapping Facts)

A fact overlaps with some other fact if the following two conditions are met:

  • There exists a substitution such that: and

  • The condition is satisfiable222Note that satisfiability is undecidable in general. This means that there might be cases where clean is unable to remove overlapping facts..

Intuitively, two facts overlap if there is some expression that can be rewritten by using any of the facts.

What clean does is to remove any overlapping between facts from the interpretation it receives as argument. It does this by conserving the most specific fact of every overlapping fact set untouched while restricting the other facts of the set so that the facts do not overlap any more. This restriction is accomplished by adding new conditions to the fact’s guard.

In order to be able to state that a fact is more specific than some other, we need an ordering:

Definition 3 (Term and Fact Ordering)

Let us define :

  • if and only if

  • if and only if

Now, this ordering can be used to compare facts.

Given two overlapping facts and , it is said that is more specific than if and only if at least one of the following criteria is met:

  • or

  • If and are a variant of each other (i.e., they are the same term with variables renamed), the fact that is more specific than the other is the one with the most restrictive guard (a guard is more restrictive than another guard if and only if entails but not viceversa).

  • If two facts are such that their patterns are a variant of each other and their guards entail each other, the fact that is more specific than the other is the one with the greatest body according to .

Remember that facts’ bodies do not contain full applications of user-defined functions, so will never be used to compare full expressions. However, may be used to compare expressions with partical applications or with predefined functions. In these cases, function symbols (both from or from ) must be treated as constructors. Note that, in a program without overlapping rules, the bodies of two overlapping facts are forced to be comparable by means of .

Definition 4 (Function clean)

Given a fact belonging to an interpretation , let us define the set
.

Considering the set for every fact , we can define (whose type is ) as:

(1)

where stands for set subtraction and:

  • . clean removes all the facts that are identically .

  • . All the facts in which are overlapped by some more specific fact are removed from and replaced by the amended fact shown above which does not overlap with any fact in .

Bool
= False
= True
= False
= Tuples
= True
=
Figure 4: Lack of unification between patterns: function nunif

The function nunif (Fig. 4) denotes lack of unification between its arguments.

Under some conditions clean will not add new facts to the given interpretation. This will happen if the guards for the facts under the big in Eq. 1 are unsatisfiable. If the program under analysis meets certain properties, this is sure to happen. Two definitions are needed to define those properties:

Definition 5 (Complete Function Definition)

A function definition for function written in the core language is said to be complete if and only if for any well typed, full application of , where the are terms there is a rule that can be used to unfold that application (that is, there exists a substitution such that and satisfiable).

Definition 6 (Productive Rule)

A program rule is said to be productive if at least one fact which is not equal to the unguarded bottom () is generated by unfolding that rule at some interpretation ( finite).

clean will not add new facts if all the function definitions in the program are complete and all the rules in the program are productive. The following Lemma states this. Note that the conditions mentioned are sufficient but not necessary.

Lemma 1 (When Can clean Drop Facts)

Let be a program without overlapping rules, whose function definitions are all complete and whose rules are all productive. Then:

For every fact which is a result of unfolding the rule , there exist in some facts which are also the result of unfolding which cover all the invocations of covered by .

The proof for this Lemma can be found in the Appendix.

We will be using the simplified version of clean whenever the program under analysis meets the criteria that have been just mentioned.

To finish this section, let us state a result that justifies why it is legal to use clean to remove overlapping facts.

Lemma 2 (Programs without Overlappings)

The fixpoint interpretation (namely, where is the unfolding operator that will be presented later) of any program without overlapping rules cannot have overlappings. is the empty interpretation.

The proof for this Lemma can be found in the Appendix.

3.2.3 Lazy Matching of Facts and Rules

=
=
= . .
=
=
       =
   
=
=
Figure 5: Function match.

The unfolding process rewrites user-defined function applications but predefined functions (including partial application) will be left unaltered by the unfolding steps since there are no rules for them. This means that when a match is sought to perform an unfolding step, the arguments to the user-defined functions may include predefined functions that must be evaluated before it is known whether they match some pattern. Such applications may also generate infinite values. Thus, we need a function match333Note that match is similar to operator =:<= proposed in [3]. that lazily matches a pattern to an expression.

Recall Fig. 2. The unfolding operator generates facts containing match whenever it finds a subexpression headed by a symbol in that needs to be matched against some rule pattern. These special facts can be thought as imposing assumptions on what the pattern must be like before proceeding.

= if there exists some unifying such that .
=
if and do not unify because there is at least a position such that is headed
by a symbol of (including @) and is not a variable.
= if and do not unify but this is not due to a predefined function symbol in .
Figure 6: umatch: Generation of matching conditions.

Those assumptions are included inside the fact’s guard. Two functions are needed in connection to those assumptions: umatch (Fig. 6) 444Observe that a function like umatch is not needed in pure Prolog since every atom is guaranteed to have a rule and lack of instantiation will cause a runtime error. generates them as a conjunction of calls to match (Fig. 5) which performs the matches at runtime.

umatch and match must be distinguished: umatch fits facts’ heads into expressions for unfolding while match is not an unfolding function; it is a function used to check (at runtime) whether certain conditions are met in evaluated expressions. umatch does not call match: umatch generates code that uses match.

The function hnf, used in the definition for match, receives an expression and returns that expression evaluated to Head Normal Form. has type .

In the result of umatch, is a list of assignments assigning values to variables inside the arguments passed to umatch and the right part of the result is a condition of the form where the are patterns and the are expressions without symbols of (they have been removed by unfolding).

The function match returns whether that matching was possible and a list of assignments from variables to expressions. The rules of match are tested from the first to the last, applying the first suitable one only.

Both lists of assignments (the ones returned by umatch or match) are not exactly substitutions because variables can be assigned to full expressions (not just terms) but they behave as such.

Two remarks must be made about match: (i) The first element of the pair returned by match is never used inside the definitions given in this paper because it is only used in order to bind variables at runtime (not at unfolding time). Those bindings will occur when a guard containing calls to match is evaluated. (ii) Therefore, match is not purely functional (i.e., it is not a side effect-free).

Example 1

(How umatch works.) Code that generates a situation like the one described is the one in Fig. 7 left. Part of its unfolding appears in Fig. 7 right 555The variables in the unfolder’s output have been renamed to ease understanding..

When the rule for app_first is unfolded, it is found that (f n) cannot be unfolded any more but it still does not match (x:xs) (the pattern in first’s rule). Therefore, the second rule for umatch imposes the assumption in the resulting fact that (f n) must match (x:xs) if the rule for app_first is to be applied. Note that f@[n] (f applied to variable n) generates an infinite term in this case. This is why match cannot be replaced by strict equality. Example 2 in the Appendix (Sect.  0.C) shows how unfolding behaves when infinite structures are generated.

a) Code that Needs Matching b) Unfolding of the Source Code
from_n::Int->[Int]
from_n n = n:(from_n(n+1))
first::[a]->a
first (x:xs) = x
app_first :: (a->[b])->a-> b
app_first f n = first(f n)
main::Int->Int
main n=app_first from_n n\end{lstlisting}
&
\begin{lstlisting}
* first(Cons(x,xs)) = x
* from_n(n) =
  Cons(n,Cons(n+1,Bot))
* app_first(f,n) |
  snd(match(Cons(x,xs),f@[n]))=x
Note: Any code preceded by * in every line has been generated by our Prolog-based unfolder. The unfolder uses Prolog terms to represent functional applications. That is why the unfolder uses tuples to represent curried applications.
Figure 7: Lazy matching of terms and rules.

3.2.4 Unfolding Operator

Operator (short form for ) where is a -interpretation is defined as shown in Fig. 8.




Figure 8: Unfolding operator

Given a program P, its meaning is given by the least fixed point of or by if the program has infinite semantics.

The auxiliary function unfold, that unfolds a rule using the facts in an interpretation, is defined in Fig. 9. The behaviour of unfold can be described as follows: unfold receives a (partially) unfolded rule (a pseudofact) which is unfolded by means of recursive calls. When the input to unfold has no invocations of user defined functions, it is just returned as it is (case 1). Otherwise, the pseudofact is unfolded by considering all the facts and positions which hold an invocation of a user-defined function (Case 2a). Those positions occupied by user-defined function calls which cannot be unfolded are replaced by (case 2b). unfold returns all the possible facts obtained by executing this procedure.


where:

  • if and otherwise.

  • if and otherwise.

Figure 9: Unfolding of a program rule using a given interpretation

When performing the unfolding of a program, unfold behaves much like the rewriting process in a TRS (i.e., it tries all the possible pairs position , fact).

To summarize, and match are the two enhancements required to write valid code for unfolding functional programs. If eager evaluation is used, these enhancements would not be necessary but naïve unfolding would still fail to work.

4 Operational Semantics

The operational semantics that describes how ground expressions written in the kernel language are evaluated is shown in Fig. 10. The semantics defines a small step relationship denoted by . The notation means that the expression can be rewritten to . The reduction relation states that can be rewritten to by using the definition of the predefined function .

The unfolding and operational semantics are equivalent in the following sense for any ground expression : where is the transitive and reflexive closure of and is in normal form according to , is a function that evaluates expressions by means of unfolding and is the limit of the interpretations found by repeatedly unfolding the program. This equivalence is proved in the Appendix, Sect. 0.B.3.

(rule)
(rulebot)
(predef)

(andtrue)

(andfalse)      (ifthenfalse)      (ifthentrue)
Figure 10: Operational Semantics

Note that this semantics is fully indeterministic; it is not meant to be used in any kind of implementation and its only purpose is to serve as a pillar for the demonstration of equivalence between the unfolding and an operational semantics. Therefore, the semantics is not lazy or greedy in itself. It is the choice of reduction positions where the semantics’ rules are apllied what will make a certain evaluation lazy or not.

5 Some Applications of the Unfolding Semantics

Declarative Debugging
666The listings of unfolded code provided in this paper have been generated by our unfolder. Source at http://www.github.com/josem-rey/unfolder and test environment at https://babel.ls.fi.upm.es/~jmrey/online_unfolder/unfolding.html

With declarative debugging, the debugger consults the internal structure of source code to find out what expressions depend on other expressions and turns this information into an Execution Dependence Tree (EDT). The debugger uses this information as well as answers from the user to blame an error on some rule. We have experimentally extended the unfolder to collect intermediate results as well as the sequence of rules that leads to every fact. This additional information allows our unfolder to build the EDT for any program run. Consider for example this buggy addition:

A1: addb Zero n = n
A2: addb Suc(Zero) n = Suc(n)
A3: addb Suc(Suc(m)) n = Suc(addb m n)
M24: main24 = addb Suc(Suc(Suc(Zero))) Suc(Zero)

We can let the program unfold until main24 is fully evaluated. This happens in , which contains the following fact for the main function (after much formatting):

root:main24 = Suc(Suc(Suc(Zero))) <M24>
  n1:addb(Suc(Suc(Suc(Zero))),Suc(Zero))=Suc(Suc(Suc(Zero)))<A3>
    n2:addb(Suc(Zero),Suc(Zero)) = Suc(Suc(Zero)) <A2>

Now, following the method described in [8], we can think of the sequence above as a 3-level EDT in which the root and node n1 contain wrong values while the node n2 is correct, putting the blame on rule A3.

The main reason that supports the use of unfolding for performing declarative debugging is that it provides a platform-independent environment to test complex programs. This platform independence can help check the limitations of some implementations (such of unreturned answers due to endless loops).

Test Coverage for a Program

It is said that a test case for a program covers those rules that are actually used to evaluate the test case. We would like to reach full code coverage with the smallest test set possible. The unfolder can be a valuable tool for finding such a test set if it is enhanced to record the list of rules applied to reach every fact.

What must be done with the enhanced unfolder is to calculate interpretations until all the rules appear at least once in the rule list associated to the facts that do not contain any and then apply a minimal set coverage algorithm to find the set of facts that will be used as the minimal test set. For example:

R1: rev [] = []    //  List inversion
R2: rev (x:xs) = append (rev xs) [x]
A1: append [] x = x
A2: append (x:xs) ys = x:(append xs ys)

The first interpretation contains:

* rev(Nil) = Nil  <R1>
* append(Nil,b) = b  <A1>
* append(Cons(b,c),d) = Cons(b,Bot)  <A2>

So, appending the empty list to any other achieves 50% coverage of append. Reversing the empty list uses 1 rule for rev: the coverage rate is 50% too. has:

* append(Cons(b,Nil),c) = Cons(b,c) <A2,A1>
...
* rev(Cons(b,Cons(c,Nil))) = Cons(c,Cons(b,Nil))
  <R2,R2,R1,A1,A2,A1>

This shows that the minimal test set to test append must consist of appending a one element list to any other list. Meanwhile, reversing a list with 2 elements achieves a 100% coverage of the code: all the rules are used.

To close this section, we would like to mention that Abstract Interpretation can be used along with unfolding to find properties of the programs under study such as algebraic or demand properties. See examples 4, 5, 6 in the Appendix.

6 Conclusion and Future Work

We have shown that unfolding can be used as the basis for the definition of a semantics for lazy, higher-order functional programs written in a kernel language of conditional equations. This is done by adapting ideas from the s-semantics approach for logic programs, but dealing with the aforementioned features was not trivial, and required the introduction of two ad-hoc primitives to the kernel language: first, a syntactic representation of the undefined and second, a matching operator that deals with partial information.

Effort has also been devoted to simplifying the code produced by the unfolder, by erasing redundant facts and constraining the shape of acceptable facts. We have provided a set of requirements for programs that ensure the safety of these simplification procedures. We have also proven the equivalence of the proposed unfolding semantics with an operational semantics for the kernel language.

We have implemented an unfolder for our kernel language. Experimenting with it supports our initial claims about a more “implementable” semantics.

Regarding future work, we want to delve into the applications that have been just hinted here, particularly declarative debugging and abstract interpretation.

Finally, we are working on a better characterization of the necessary conditions that functional programs must meet in order for different optimized versions of the clean method to work safely.

References

  • [1] Alpuente, M., Falaschi, M., Vidal, G.: Narrowing-driven Partial Evaluation of Functional Logic Programs. In: Proc. ESOP’96. LNCS, vol. 1058. Springer (1996)
  • [2] Alpuente, M., Falaschi, M., Moreno, G., Vidal, G.: Safe folding/unfolding with conditional narrowing. In: Proc. ALP’97. pp. 1–15. Springer LNCS (1997)
  • [3] Antoy, S., Hanus, M.: Declarative programming with function patterns. In: Proc. of LOPSTR’05. pp. 6–22. Springer LNCS (2005)
  • [4] Bossi, A., Gabbrielli, M., Levi, G., Martelli, M.: The s-semantics approach: Theory and applications. Journal of Logic Programming 19/20, 149–197 (1994)
  • [5] Burstall, R.M., Darlington, J.: A transformation system for developing recursive programs. J. ACM 24(1), 44–67 (Jan 1977)
  • [6] Hanus, M.: The integration of functions into logic programming: From theory to practice. Journal of Logic Programming pp. 583–628 (1994)
  • [7] Pettorossi, A., Proietti, M.: Perfect model checking via unfold/fold transformations. In: Computational Logic, LNCS 1861. pp. 613–628. Springer (2000)
  • [8] Pope, B., Naish, L.: Buddha - A declarative debugger for Haskell (1998)
  • [9] Scott, D.: The lattice of flow diagrams (Nov 1970)

Appendix

This appendix is not part of the submission itself and is provided just as supplementary material for reviewers. It pursues the following goals:

  1. To provide a pictorical representation of the functions involved in the unfolding process, which hopefully helps in grasping how the whole process works (Sect. A).

  2. To describe in what sense the unfolding and the operational semantics are equivalent and to prove such equivalence (Sect. B).

  3. To present a larger example that intends to clarify how the functions that have been used actually work as well as additional examples (Sect. C).

  4. To establish some results that support the validity of the code generated by the unfolder (Sect. D).

Appendix 0.A Pictorial Representation of the Unfolding Process

clean

unfold

eval

umatch

match

Runtime

Invocation

Data flow

Code
Figure 11: Relation among the functions taking part in unfolding

Throughout Sect. 3 a number of auxiliary functions were presented. These functions are depicted in Fig. 11. The figure can be explained as follows:

The starting point is . does nothing but to call unfold and remove the redundant facts by calling clean. It is then up to the user to call again to perform another step in the unfolding process.

The second level of the figure shows unfold, which takes a program rule and unfolds it as much as possible. unfold calls itself with the output of its previous execution until no more positions are left to unfold (arrow pointing downwards). If unfold receives an input where at least one position is unfoldable, it calls eval on the arguments of the unfoldable expression and then calls umatch to perform the actual fitting between the unfoldable position and the head of some fact.

The last level of the figure (below the dashed line) represents the execution of the unfolded code. This part is not related with the definition of the unfolding operator, but with the execution of the unfolded code. The code is made of the output of unfold whose guards are (possibly) extended with , the output from umatch, which contains the invocations to match. Observe that the output from umatch goes to the generated code only, not to the unfolding process.

To the best of our knowledge, this unfolding process is a first effort to formulate an unfolding operator beyond naïve unfolding.

Appendix 0.B Equivalence between the Unfolding Semantics and the Operational Semantics

0.b.1 Unfolding of an Expression

Let us define a function that finds what is the normal form for a given expression by means of unfolding. In short, what ueval does is to evaluate a given (guarded) expression by unfolding it according to a given interpretation.

The function ueval has type and is defined as shown in Fig. 12.

=   if no rule from applies to any position of
=    such that a rule of is applicable to .
- - - - - - - - - - - - -
= t
if
=
if
=
=
=
=
=
if with
=
if with
Figure 12: The function: Evaluating expressions by means of unfolding

Note that any expression is equivalent to ().

0.b.2 Trace of a Fact or an Expression

Given a fact , belonging to any interpretation , its trace is the list of pairs where