Time Equations for
Lazy Functional (Logic) Languages⋆
Elvira Albert1 , Josep Silva2 , and Germán Vidal2
1
DSIP, Universidad Complutense de Madrid
Email: elvira@sip.ucm.es
2
DSIC, Technical University of Valencia
Email: {jsilva,gvidal}@dsic.upv.es
Abstract. There are very few approaches to measure the execution
costs of lazy functional (logic) programs. The use of a lazy execution
mechanism implies that the complexity of an evaluation depends on its
context, i.e., the evaluation of the same expression may have different
costs depending on the degree of evaluation required by the different
contexts where it appears. In this paper, we present a novel approach to
complexity analysis of functional (logic) programs. We focus on the construction of equations which compute the time-complexity of expressions.
In contrast to previous approaches, it is simple, precise—i.e., it computes
exact costs rather than upper/lower bounds—, and fully automatic.
1
Introduction
There are very few approaches to formally reason about program execution costs
in the field of lazy functional (logic) languages. Most of these approaches analyze the cost by first constructing (recursive) equations which describe the
time-complexity of a given program and then producing a simpler cost function (ideally as a closed form) by some (semi-)automatic manipulation of these
equations. In this paper, we concentrate on the first part of this process: the
(automatic) construction of equations which compute the time-complexity of a
given lazy functional (logic) program.
Laziness introduces a considerable difficulty in the time-complexity analysis
of functional (logic) programs. In eager (call-by-value) languages, the cost of
nested applications, e.g., f (g(v)), can be expressed as the sum of the costs of
evaluating g(v) and f (v ′ ), where v ′ is the result returned by the call g(v). In a
lazy (call-by-name) language, g(v) is only evaluated as much as needed by the
outermost function f (in the extreme, it could be no evaluated at all). Therefore, one cannot determine (statically) the cost associated to the evaluation of
the arguments of function calls. There exist several methods which simplify this
problem by transforming the original program into an equivalent one under a
call-by-value semantics and, then, by analyzing the transformed program (e.g.,
⋆
This work has been partially supported by CICYT TIC 2001-2705-C03-01, by the
Generalitat Valenciana CTIDIA/2002/205 and by the MCYT HA2001-0059.
[13]). The translation, however, makes the program significantly more complex
and the number of evaluation steps is not usually preserved through the transformation. A different approach is taken by [14, 17], where the generated timeequations give upper (resp. lower) bounds by using a strictness (resp. neededness)
analysis. In particular, [17] may produce equations which are non-terminating
even if the original program terminates; in contrast, [14] guarantees that the
time-equations terminate at least as often as the original program. As an alternative approach, there are techniques (e.g., [6]) which produce time equations
which are precise, i.e., they give an exact time analysis. In this case, the drawback
is that the time equations cannot be solved mechanically.
In this work, we introduce a novel program transformation which produces
(automatically) time equations which are precise—i.e., give an exact time—and,
moreover, they can be executed in the same language of the source program—i.e.,
we define a source-to-source transformation. To the best of our knowledge, this
is the first approach which achieves both goals in the context of a lazy language.
To be more precise, our aim is to transform a program so that it computes
not only values but their associated time-complexities. More formally, given a
lazy (call-by-name) semantics Sem and its cost-augmented version Semcost , we
want to define a program transformation trans(P ) = Pcost such that the execution of program P with the cost-augmented semantics gives the same result
as the execution of the transformed program Pcost with the standard semantics,
i.e., Semcost (P ) = Sem(Pcost ). Let us note that the considered problem is decidable. First, it is not difficult to construct a cost-augmented semantics which
precisely computes the cost of a program’s execution (see, e.g., Section 2). Then
the Futamura projections [7] ensure the existence of the desired transformed
program Pcost : the partial evaluation [10] of Semcost w.r.t. P gives a program
P ′ such that P ′ produces the same outputs as Semcost (P ), i.e.,
Sem(P, input) = output
Semcost (P, input) = (output, cost)
mix(Semcost , P ) = P ′ with P ′ (input) = (output, cost)
where mix denotes a partial evaluator. Therefore, Pcost (equivalently, P ′ in the
above equations) exists and can be effectively constructed. However, by using a
partial evaluator for the considered language (e.g., [3, 4]), Pcost will be probably
a cost-augmented interpreter slightly extended to only consider the rules of a
particular program P . Therefore, similarly to the cost-augmented interpreter,
the program Pcost generated in this way will still use a sort of “ground representation” to evaluate expressions. This makes the complexity analysis of the
program Pcost as difficult as the analysis of the original program (or even harder).
Furthermore, its execution will be almost as slow as running the cost-augmented
interpreter with the original program P (which makes it infeasible for profiling).
Our main concern in this work is to provide a more practical transformation.
In particular, our new approach produces simple equations, it gives exact timecomplexities, and it is fully automatic. To the best of our knowledge, this is
the first transformation fulfilling such properties. For the sake of generality, our
developments are formalized in the context of a lazy functional logic language.
Program:
Function definition:
Expression:
Pattern:
P ::= D1 . . . Dm
D ::= f (x1 , . . . , xn ) = e
e ::= x
| c(x1 , . . . , xn )
| f (x1 , . . . , xn )
| let x1 = e1 , . . . , xn = en in e
| case x of {p1 → e1 ; . . . ; pn → en }
| fcase x of {p1 → e1 ; . . . ; pn → en }
p ::= c(x1 , . . . , xn )
(variable)
(constructor call)
(function call)
(let binding)
(rigid case)
(flexible case)
Fig. 1. Syntax of lazy functional logic programs
In particular, the use of logical variables may be useful to analyze the complexity
of the time-equations (as we will discuss in Section 4.1). Nevertheless, our ideas
can be directly applied to a pure (first-order) lazy functional language.
The paper is organized as follows. Section 2 introduces the syntax and costaugmented semantics of the considered lazy functional logic language. In Sect. 3,
we present our program transformation and state its correctness. The usefulness
of the transformation is illustrated in Sect. 4 and several applications are outlined. Finally, Sect. 5 concludes and gives some directions for future work.
2
Language Syntax and Cost Semantics
In this section, we introduce the syntax of our lazy functional logic language as
well as a precise characterization of its cost semantics. A survey of functional
logic languages can be found in [8].
Functional Logic Programs. The syntax for programs is shown in Figure 1.
A program P consists of a sequence of function definitions D such that the
left-hand side has pairwise different variable arguments. The right-hand side
contains variables X = {x, y, z, . . .}, data constructors (e.g., a, b, c,. . . ), userdefined functions (e.g., f , g, h,. . . ), case expressions, and let bindings where
the local variables x1 , . . . , xn are only visible in e1 , . . . , en , e. Note that we only
consider function and constructor calls whose arguments are all variables (not
necessarily different). This is not a restriction in practice since several simple
normalization algorithms exist (see, e.g., [2]). Basically, these algorithms proceed
by denoting all non-variable arguments in function or constructor calls by means
of let bindings. In this work, we only consider programs with no extra-variables,
i.e., all the free variables in the right-hand side of a program rule must appear
in the left-hand side. A case expression has the following form:3
(f )case e of {c1 (xn1 ) → e1 ; . . . ; ck (xnk ) → ek }
3
We write on for the sequence o1 , . . . , on and (f )case for either fcase or case.
(VarCons)
(VarExp)
(Val)
(Fun)
Γ [x 7→ t] : x ⇓0 Γ [x 7→ t] : t
where t is constructor-rooted
Γ [x 7→ e] : e ⇓k ∆ : v
where e is not constructor-rooted
and e 6= x
Γ [x 7→ e] : x ⇓k ∆[x 7→ e] : v
where v is constructor-rooted
Γ : v ⇓0 Γ : v
or a variable with Γ [v] = v
Γ : ρ(e) ⇓k ∆ : v
where f (yn ) = e ∈ P and ρ = {yn 7→ xn }
Γ : f (xn ) ⇓k+1 ∆ : v
(Let)
Γ [yk 7→ ρ(ek )] : ρ(e) ⇓k ∆ : v
Γ : let {xk = ek } in e ⇓k ∆ : v
(Select)
Γ : e ⇓k1 ∆ : c(yn )
∆ : ρ(ei ) ⇓k2 Θ : v
Γ : (f )case e of {pk → ek } ⇓k1 +k2 Θ : v
(Guess)
where ρ = {xk 7→ yk }
and yk are fresh variables
where pi = c(xn )
and ρ = {xn 7→ yn }
Γ : e ⇓k1 ∆ : x
∆[x 7→ ρ(pi ), yn 7→ yn ] : ρ(ei ) ⇓k2 Θ : v
Γ : fcase e of {pk → ek } ⇓k1 +k2 Θ : v
where pi = c(xn ), ρ = {xn 7→ yn }, and yn are fresh variables
Fig. 2. Step-Counting Semantics for Lazy Functional Logic Programs
where e is an expression, c1 , . . . , ck are different constructors, and e1 , . . . , ek
are expressions. The pattern variables xni are locally introduced and bind the
corresponding variables of the subexpression ei . The difference between case
and fcase only shows up when the argument e is a free variable: case suspends
whereas fcase non-deterministically binds this variable to the pattern in a branch
of the case expression.
Programs which follow the syntax of Figure 1 only differs from typical lazy
(first-order) functional programs in the use of flexible case expressions. Therefore,
our developments apply straightforwardly to pure lazy functional programs.
A Step-Counting Semantics. We base our developments on the big-step semantics of [2] for (first-order) lazy functional logic programs. The only difference
is that our cost-augmented semantics does not model sharing, since then the generated time-equations would be much harder to analyze (see Section 4.2). In the
following, we present an extension of the big-step semantics of [2] in order to
count the number of function unfoldings. The step-counting (state transition)
semantics for lazy functional logic programs is defined in Figure 2. Our rules
obey the following naming conventions:
Γ, ∆, Θ ∈ Heap = Var → Exp
v ∈ Value ::= x | c(en )
A heap is a partial mapping from variables to expressions (the empty heap is
denoted by [ ]). The value associated to variable x in heap Γ is denoted by Γ [x].
Γ [x 7→ e] denotes a heap with Γ [x] = e, i.e., we use this notation either as a
condition on a heap Γ or as a modification of Γ . In a heap Γ , a logical variable
x is represented by a circular binding of the form Γ [x] = x. A value is a term in
so called head normal form, i.e., a constructor-rooted term or a logical variable
(w.r.t. the associated heap).
We use judgments of the form “Γ : e ⇓k ∆ : v” which are interpreted as
“the expression e in the context of the heap Γ evaluates to the value v with the
(possibly modified) heap ∆ by unfolding k function calls”. We briefly explain
the rules of our semantics:
(VarCons) In order to evaluate a variable which is bound to a constructor-rooted
term in the heap, we simply reduce the variable to this term. The heap
remains unchanged and the associated cost is trivially zero.
(VarExp) If the variable to be evaluated is bound to some expression in the
heap, then this expression is evaluated and, then, this value is returned as
the result. The cost is not affected by the application of this rule.
It is important to notice that this rule is different from the one presented in
[2]. In particular, in the original big-step semantics, it is defined as follows:
Γ [x 7→ e] : e ⇓k ∆ : v
Γ [x 7→ e] : x ⇓k ∆[x 7→ v] : v
which means that the heap is updated with the computed value v for e. This
is essential to correctly model sharing (trivially, our modification does not
affect to the computed results, only to the number of function unfoldings).
The generation of time-equations that correctly model sharing is a difficult
topic which is out of the scope of this paper (see Section 4.2).
(Val) For the evaluation of a value, we return it without modifying the heap,
with associated cost zero.
(Fun) This rule corresponds to the unfolding of a function call. The result is
obtained by reducing the right-hand side of the corresponding rule. We assume that the considered program P is a global parameter of the calculus.
Trivially, if the evaluation of the unfolded right-hand side requires k function
unfoldings, the original function call will require k + 1 function calls.
(Let) In order to reduce a let construct, we add the bindings to the heap and
proceed with the evaluation of the main argument of let. Note that we rename the variables introduced by the let construct with fresh names in order
to avoid variable name clashes. The number of function unfoldings is not affected by the application of this rule.
(Select) This rule corresponds to the evaluation of a case expression whose argument reduces to a constructor-rooted term. In this case, we select the
appropriate branch and, then, proceed with the evaluation of the expression
in this branch by applying the corresponding matching substitution. The
number of function unfoldings is obtained by adding the number of function
unfoldings required to evaluate the case argument with those required to
evaluate the expression in the selected branch.
(Guess) This rule considers the evaluation of a flexible case expression whose
argument reduces to a logical variable. It non-deterministically binds this
variable to one of the patterns and proceeds with the evaluation of the corresponding branch. Renaming of pattern variables is also necessary to avoid
variable name clashes. Additionally, we update the heap with the (renamed)
logical variables of the pattern. Similarly to the previous rule, the number of
function unfoldings is the addition of the function unfoldings in the evaluation of the case argument and the (non-deterministically) selected branch.
A proof of a judgment corresponds to a derivation sequence using the rules of
Figure 2. Given a program P and an expression e (to be evaluated), the initial
configuration has the form “[ ] : e”. If the judgment “[ ] : e ⇓k Γ : v” holds,
then the computed answer can be extracted from the final heap Γ by a simple
process of dereferencing in order to obtain the values associated to the logical
variables of the initial expression e.
In the following, we denote by a judgment of the form Γ : e ⇓ ∆ : v
a derivation with the standard semantics (without cost), i.e., the semantics of
Figure 2 by ignoring the cost annotations.
3
The Program Transformation
In this section, we present our program transformation to compute the time
complexity of lazy functional logic programs. First, let us informally describe
the approach of [17] in order to motivate our work and its advantages (the
approach of [14] is basically equivalent except for the use of a neededness analysis
rather than a strictness analysis). In this method, for each function definition
f (x1 , . . . , xn ) = e, a new rule of the form
cf (x1 , . . . , xn , α) = α ֒→ 1 + T [[e]]α
is produced, where cf is a time function and α denotes an evaluation context
which indicates whether the evaluation of a given expression is required. In this
way, transformed functions are parameterized by a given context. The righthand side is “guarded” by the context so that the corresponding cost is not
added when its evaluation is not required. A key element of this approach is
the propagation of contexts through expressions. For instance, the definition of
function T for function calls is as follows:
T [[f (e1 , . . . , en )]]α = T [[e1 ]](f ♯1 α) + . . . + T [[en ]](f ♯n α) + cf (e1 , . . . , en , α)
The basic idea is that the cost of evaluating a function application can be obtained from the cost of evaluating the strict arguments of the function plus the
cost of the outermost function call. This information is provided by a strictness
analysis. Recall that a function is strict in some argument i iff f (. . . , ⊥i , . . .) = ⊥,
where ⊥ denotes an undefined expression (i.e., the result of function f cannot
be computed when the argument i cannot be evaluated to a value).
Functions f ♯i are used to propagate strictness information through function
arguments. To be more precise, they are useful to determine whether argument
i of function f is strict in the context α. Let us illustrate this approach with an
example. Consider the following function definitions:4
head xs = case xs of {(y : ys) → y}
foo xs = let ys = head xs in head ys
4
Although we consider a first-order language, we use a curried notation in examples,
as it is common practice in functional programming.
C[[x]]
C[[c(x1 , . . . , xn )]]
C[[f (x1 , . . . , xn )]]
C[[(f )case x of {pn → en }]]
C[[let yn = en in e]]
=
=
=
=
=
x
(c(x1 , . . . , xn ), 0)
fc (x1 , . . . , xn )
(f )case x of {(pn , kn ) → C[[en ]] + kn }
let yn = C[[en ]] in C[[e]]
Fig. 3. Cost Function C
According to [17], these functions are transformed into
chead xs α = α ֒→ 1
cfoo xs α = α ֒→ 1 + chead xs (head♯1 α)
+ let ys = head xs in chead (ys α)
If the strictness analysis has a good precision, then whenever this function is
called within a strict context, (head♯1 α) should indicate that the head normal
form of the argument of head is also needed.
Unlike the previous transformation, we do not use the information of a strictness (or neededness) analysis. A novel idea of our method is that we delay the
computation of the cost of a given function up to the point where its evaluation is
actually required. This contrasts to [14, 17] where the costs of the arguments of a
function call are computed a priori, hence the need of the strictness information.
The following definition formalizes our program transformation:
Definition 1 (time-equations). Let P be a program. For each program rule
of the form f (x1 , . . . , xn ) = e ∈ P , we produce a transformed rule:
fc (x1 , . . . , xn ) = C[[e]] + 1
where the definition of the cost function C is shown in Figure 3. Additionally,
we add the following function “+” to the transformed program:
(c, k1 ) + k2 = (c, k1 + k2 )
for all constructor c/0 in P
(c(xn ), k1 ) + k2 = (c(xn ), k1 + k2 ) for all constructor c/n in P
Basically, for each constructor term c(xn ) computed in the original program, a
pair of the form (c(x′n ), k) is computed in the transformed program, where k is
the number of function unfoldings which are needed to produce the constructor “c”. For instance, a constructor term—a list—like (x : y : []) will have the
form (x : (y : ([], k3 ), k2 ), k1 ) in the transformed program, where k1 is the cost
of producing the outermost constructor “:”, k2 the cost of producing the innermost constructor “:” and k3 the cost of producing the empty list. The auxiliary
function “+” is introduced to correctly increment the current cost.
The definition of the cost function C distinguishes the following cases depending on the structure of the expression in the right-hand side of the program
rule, as it is depicted in Figure 3:
– Variables are not modified. Nevertheless, observe that now their domain is
different, i.e., they denote pairs of the form (c(xn ), k).
– For constructor-rooted terms, their associated cost is always zero, since no
function unfolding is needed to produce them.
– Each function call f (x1 , . . . , xk ) is replaced by its counterpart in the transformed program, i.e., a call of the form fc (x1 , . . . , xn ).
– Case expressions are transformed by leaving the case structure and then
replacing each pattern pi by the pair (pi , ki ), where ki denotes the cost
of producing the pattern pi (i.e., the cost of evaluating the case argument
to some head normal form). Then, this cost is added to the transformed
expression in the selected branch.
– Finally, let bindings are transformed by applying recursively function C to
all the expressions.
Theorem 1 states the correctness of our approach. In this result, we only consider
ground expressions—without free variables—as initial calls, i.e., we only consider
“functional” computations. The use of non-ground calls will be discussed in
Section 4.1. Below, we denote by ∆c the heap obtained from ∆ by replacing
every binding x 7→ e in ∆ by x 7→ C[[e]].
Theorem 1. Let P be a program and let Pcost be its transformed version obtained by applying Definition 1. Then, given a ground expression e, we have
[ ] : e ⇓k ∆ : v w.r.t. P iff [ ] : C[[e]] ⇓ ∆c : (C[[v]], k) w.r.t. Pcost .
Roughly speaking, we obtain the same cost by evaluating an expression w.r.t.
the original program and the cost-augmented semantics defined in Figure 2 and
by evaluating it w.r.t. the transformed program and the standard semantics.
4
The Transformation in Practice
In this section, we illustrate the usefulness of our approach by means of some
selected examples and point out possible applications of the new technique.
4.1
Complexity Analysis
The generated time-equations are simple and, thus, they are appropriate to
reason about the cost of evaluating expressions in our lazy language. We consider
an example which shows the behavior of lazy evaluation:
hd xs
from x
= case xs of {(y : ys) → y}
= let z = S x
y = from z
in x : y
nth n xs = case n of {Z → hd xs; (S m) → case xs of {(y : ys) → nth m ys}}
where natural numbers are built from Z and S. Function hd returns the first
element of a list, function from computes an infinite list of consecutive numbers
(starting at its argument), and function nth returns the element in the n-th
position of a given list. The transformed program is as follows:
hdc xs
= case xs of {(y : ys, kxs ) → y + kxs } + 1
fromc x = let z = (S x, 0), y = fromc z in (x : y, 0) + 1
nthc n xs = case n of
{(Z, kn ) → hdc xs + kn ;
(S m, kn ) → case xs of
{(y : ys, kxs ) → nthc m ys + kxs } + kn } + 1
Consider, for instance, the function call5 “nth (S (S (S Z))) (from Z)”, which
returns the value (S (S (S Z))) with 9 function unfoldings. Consequently, the
transformed call “nthc (S (S (S (Z, 0), 0), 0), 0) (from (Z, 0))” returns the pair
((S (S (S (Z, 0), 0), 0), 0), 9). Let us now reason about the complexity of the original program. First, the equations of the transformed program can easily be
transformed as follows:6
hdc (y : ys, kxs )
= y + (kxs + 1)
fromc x
= (x : fromc (S x, 0), 1)
nthc (Z, kn ) xs
= (hdc xs) + (kn + 1)
nthc (S m, kn ) (y : ys, kxs ) = (nthc m ys) + (kxs + kn + 1)
Here, we basically used inlining (for let expressions) and have moved the arguments of the case expressions to the left-hand sides of the rules. Now, we consider
a generic call of the form nthc v1 (from v2 ), where v1 and v2 are constructor
terms (hence all the associated costs are zero). By unfolding the call to fromc
and replacing kn by 0 (since v1 is a constructor term), we get:
nthc (Z, 0) (x : fromc (S x, 0), 1) = (hdc (x : fromc (S x, 0), 1)) + 1
nthc (S m, 0) (x : fromc (S x, 0), 1) = (nthc m (fromc (S x, 0))) + 2
By unfolding the call to hdc in the first equation, we have:
nthc (Z, 0) (x : fromc (S x, 0), 1) = x + 3
nthc (S m, 0) (x : fromc (S x, 0), 1) = (nthc m (fromc (S x, 0))) + 2
Therefore, each recursive call to nthc requires 2 steps, while the base case requires 3 steps, i.e., the total cost of a call nth v1 (from v2 ) is n ∗ 2 + 3, where n
is the natural number represented by v1 .
A theorem prover may help to perform this task, this is an interesting (and
difficult) problem for further research. Nevertheless, we think that logical variables may help to perform this task. For instance, if one executes the function
call nthc x (fromc (Z, 0)), where x is a logical variable, we get the following
(infinite) sequence (computed bindings for x are applied to the initial call):
nthc (Z, n1 ) (fromc (Z, 0))
=⇒ (Z, 3 + n1 )
nthc (S (Z, n1 ), n2 ) (fromc (Z, 0))
=⇒ (S (Z, 0), 5 + n1 + n2 )
nthc (S (S (Z, n1 ), n2 ), n3 ) (fromc (Z, 0)) =⇒ (S (S (Z, 0), 0), 7 + n1 + n2 + n3 )
...
5
6
Here, we inline the let bindings to ease the reading of the function calls.
Since we only consider ground initial calls—and program rules have no extravariables—we know that case expressions will never suspend.
If one considers that n1 = n2 = n3 = 0 (i.e., that the first input argument is
a constructor term), then it is fairly easy to check that 2 steps are needed for
each constructor “S” of the first input argument of nthc , and 3 more steps for
the final constructor “Z”. Therefore, we have 2 ∗ n + 3, where n is the natural
number represented by the first input argument of nthc (i.e., we obtain the same
result as with the previous analysis).
On the other hand, there are already some practical systems, like ciaopp [9],
which are able to solve certain classes of equations. They basically try to match
the produced recurrence with respect to some predefined “patterns of equations”
which represent classes of recurrences whose solution is known. These ideas could
be also adapted to our context.
4.2
Other Applications and Extensions
Here we discuss some other practical applications which could be done by extending the developments in this work.
There are several approaches in the literature to symbolic profiling (e.g., [5,
15]). Here, the idea is to extend the standard semantics with the computation of
cost information (similarly to our step-counting semantics, but including different cost criteria like case evaluations, function applications, etc). A drawback of
this approach is that one should modify the language interpreter or compiler in
order to implement it. Unfortunately, this possibility usually implies a significant
amount of work. An alternative could be the development of a meta-interpreter
extended with the computation of symbolic costs, but the execution of such a
meta-interpreter would involve a significant overhead at runtime.
In this context, our program transformation could be seen as a preprocessing
stage (during compilation), which produces an instrumented program whose execution (in a standard environment) returns not only the computed values but
also the costs of computing such values. However, in this approach, the profiling results should take into account sharing, since almost all implementations of
lazy functional (logic) languages allow the sharing of common variables. In this
case, we can still use our time-equations by performing an additional phase of
linearization for the right-hand sides of those rules with multiple (free) occurrences of the same variable. Consider, for instance, the following simple program:
main = let x = f, y = g x in h x y
h x y = case x of {Z → case y of {Z → Z}}
gx
= x
f = Z
The (slightly simplified) time-equations for this program are as follows:
mainc = let x = fc , y = gc x in (hc x y) + 1
hc x y = case x of {(Z, kx ) → case y of {(Z, ky ) → (Z, kx + ky + 1)}}
gc x
= x+1
fc = (Z, 1)
If we do not consider sharing, the evaluation of function main requires 5 function unfoldings (mainc , gc , hc , and fc twice). Our equations correctly model this
cost. However, under a sharing-based implementation, the evaluation of main
only requires 4 function unfoldings, since function fc is only unfolded once. We
can tackle this situation by introducing new logical variables for shared variables
and then imposing the constraint that all shared variables but one must have an
associated cost of zero. For instance, the rule for mainc is now transformed to:
mainc = let x = fc , y = gc x′ in (h′c x′ x y) + 1
where x′ is a new logical variable and function h′c is defined in the following way:
h′c x′ x y | one x x′ = h x y
This is a guarded rule, where the right-hand side is only evaluated if the constraint one x x′ succeed. The definition of one should ensure that there is (at
most) one cost which is not zero:
one (Z, 0) (Z, 0) = success
one (Z, 0) (Z, k) = k > 0
one (Z, k) (Z, 0) = k > 0
This idea can be generalized so that the generated time-equations are still a
good basis to perform symbolic profiling of lazy functional logic languages which
model sharing. Of course, this will make the transformed program much harder
to understand and analyze. Nevertheless, it will be only used internally by the
language environment to generate profiling results, so this is not a problem.
Finally, the proposed transformation could also be useful in the context of
program debugging. In particular, some abstract interpretation based systems,
like ciaopp [9], allows the user to include annotations in the program with the
predicted cost. This cost will be then compared with the one automatically
generated by the compiler (here, our program transformation can be useful to
compute this cost). If both cost functions are not equivalent, this might indicate a
programmer error; for instance, the program may contain undesired loops which
directly affect the time complexity.
5
Conclusions
In this paper we introduced a novel program transformation to measure the cost
of lazy functional (logic) computations. This is a difficult task mainly due to the
on-demand nature of lazy evaluation. Indeed, previous approaches to this problem only compute an approximation of the result or give exact time equations
which cannot be mechanically solved. Our approach is simple, precise—i.e., it
computes exact costs rather than upper/lower bounds—, and fully automatic. In
order to check the applicability of the ideas presented in this paper, a prototype
implementation of the program transformation has been undertaken.
There are several possibilities for future work. In this work, we sketched the
use of logical variables for reasoning about the complexity of a program. This
idea can be further developed and, perhaps, combined with partial evaluation
so that we get a simpler representation of the time-equations for a given expression. The extension of our developments to cope with higher-order functions
seems essential to be able to analyze typical lazy functional (logic) programs. For
instance, this can be done by defunctionalization, i.e., by including an explicit
(first-order) application operator. Finally, time-equations can be augmented with
more cost information so that they can be used for profiling. In this case, avoiding the multiple computation of costs for shared variables is required. As we
mentioned before, this can be done by linearization of the right-hand sides with
new logical variables and, then, by introducing appropriate constraints on these
logical variables.
References
1. E. Albert, S. Antoy, and G. Vidal. Measuring the Effectiveness of Partial Evaluation in Functional Logic Languages. In Proc. of LOPSTR 2000, pages 103–124.
Springer LNCS 2042, 2001.
2. E. Albert, M. Hanus, F. Huch, J. Oliver, and G. Vidal. An Operational Semantics
for Declarative Multi-Paradigm Languages. In Proc. of WRS 2002, volume 70(6)
of ENTCS. Elsevier Science Publishers, 2002.
3. E. Albert, M. Hanus, and G. Vidal. A Practical Partial Evaluation Scheme for
Multi-Paradigm Declarative Languages. Journal of Functional and Logic Programming, 2002(1), 2002.
4. E. Albert and G. Vidal. The Narrowing-Driven Approach to Functional Logic
Program Specialization. New Generation Computing, 20(1):3–26, 2002.
5. E. Albert and G. Vidal. Symbolic Profiling of Multi-Paradigm Declarative Languages. In Proc. of LOPSTR’01, pages 148–167. Springer LNCS 2372, 2002.
6. B. Bjerner and S. Holmström. A Compositional Approach to Time Analysis of
First-Order Lazy Functional Programs. In Proc. of FPCA’89. ACM Press, 1989.
7. Yoshihiko Futamura. Partial Evaluation of Computation Process—An Approach
to a Compiler-Compiler. Higher-Order and Symbolic Computation, 12(4):381–391,
1999. Reprint of article in Systems, Computers, Controls 1971.
8. M. Hanus. The Integration of Functions into Logic Programming: From Theory
to Practice. Journal of Logic Programming, 19&20:583–628, 1994.
9. M. Hermenegildo, G. Puebla, F. Bueno, and P. López-Garcı́a. Program Development Using Abstract Interpretation (and The Ciao System Preprocessor). In 10th
International Static Analysis Symposium (SAS’03). Springer-Verlag, June 2003.
10. N.D. Jones, C.K. Gomard, and P. Sestoft. Partial Evaluation and Automatic Program Generation. Prentice-Hall, Englewood Cliffs, NJ, 1993.
11. J. Launchbury. A Natural Semantics for Lazy Evaluation. In Proc. of POPL’93,
pages 144–154. ACM Press, 1993.
12. Y.A. Liu and G. Gómez. Automatic accurate time-bound analysis for high-level
languages. In Proc. of the ACM SIGPLAN 1998 Workshop on Languages, Compilers, and Tools for Embbeded Systems, pages 31–40. Springer LNCS 1474, 1998.
13. D. Le Métayer. Analysis of Functional Programs by Program Transformations.
In Proc. of 2nd France-Japan Artificial Intelligence and Computer Science Symposium. North-Holland, 1988.
14. D. Sands. Complexity Analysis for a Lazy Higher-Order Language. In Proc. of
ESOP’90. Springer LNCS 432, 1990.
15. P.M. Sansom and S.L. Peyton-Jones. Formally Based Profiling for Higher-Order
Functional Languages. ACM TOPLAS, 19(2):334–385, 1997.
16. G. Vidal. Cost-Augmented Narrowing-Driven Specialization. In Proc. of PEPM’02,
pages 52–62. ACM Press, 2002.
17. P. Wadler. Strictness Analysis aids Time Analysis. In Proc. of POPL’88. ACM
Press, 1988.