[Haskell-cafe] Re: appending an element to a list
apfelmus
apfelmus at quantentunnel.de
Tue Jun 3 05:14:58 EDT 2008
Abhay Parvate wrote:
> I somehow thought it would be easy to talk about complexity of calculating
> individual elements in an infinite list should be sufficient, but that seems
> to be involved, and my over-generalization doesn't seem to work. Thanks for
> the link; particularly it has reference to Wadler's papers exactly on this
> problem.
Note however that Wadler's and similar formalisms are still a
unsatisfactory in that they are quite clumsy to work with, it's quite
tedious/impossible to analyze examples with a lot of lazy evaluation.
But they are a good guideline.
In his book about purely functional data structures [1], Okasaki takes a
different approach; each node of a data structure is given a debit, a
cost to evaluate it. For instance, consider
xs = x1 : x2 : x3 : ... : xn : []
1 1 1 ... 1 1 0
ys = y1 : y2 : y3 : ... : ym : []
1 1 1 ... 1 1 0
The numbers below indicate the time it takes to evaluate the node to
weak head normal form. For demonstration purposes, I arbitrarily chose 1
for each (:) here.
The combined list will then have debits like
xs ++ ys = x1 : x2 : x3 : ... : xn : y1 : y2 : y3 : ... : ym : []
2 2 2 ... 2 2 1 1 1 ... 1 1 0
In other words, the ys list is copied verbatim but each element of xs
incurs an additional cost of 1, corresponding to one step in the
evaluation of the concatenation with (++).
In order to force/inspect a constructor/node, you have to pay off its
debits first. In the above example, head (xs ++ ys) would have to pay
2 units of time (one unit for head xs and one for the (++)). Now, the
thing about debits is that we can relocate them to the top and only
overestimate the total running time if we do that. For instance, we
could push all debits to the top
xs ++ ys = x1 : x2 : x3 : ... : xn : y1 : y2 : y3 : ... : ym : []
2n+m 0 0 ... 0 0 0 0 0 ... 0 0 0
so that evaluating head (xs ++ ys) is now estimated to cost (2n+m)
units of time while the rest is free/fully evaluated.
The above example is rather useless, but consider the case n == m and
xs = x1 : x2 : x3 : ... : xn : []
0 0 0 ... 0 0 0
ys = y1 : y2 : y3 : ... : yn : []
0 0 0 ... 0 0 0
i.e. two fully evaluated lists of the same length. Then, we have
xs ++ reverse ys =
x1 : x2 : x3 : ... : xn : yn : y{n-1} : ... : y1 : []
1 1 1 ... 1 1 n 0 ... 0 0 0
because reversing the list ys is "monolithic", i.e. looking at its head
already forces the tail of the list. But now, we can distribute the
debits upwards
xs ++ reverse ys =
x1 : x2 : x3 : ... : xn : yn : y{n-1} : ... : y1 : []
2 2 2 ... 2 2 0 0 ... 0 0 0
and thereby amortize the cost of reversing the second lists over the n
elements of the first list. This is used in the implementation of purely
functional queues, see also Okasaki's book.
[1]: Chris Okasaki. Purely Function Data Structures.
http://www.cs.cmu.edu/~rwh/theses/okasaki.pdf
(This is the thesis on which the book is based.)
Regards,
apfelmus
More information about the Haskell-Cafe
mailing list