[Haskell-cafe] Re: flip fix and iterate
claus.reinke at talk21.com
Tue Mar 20 22:21:00 EDT 2007
> Haskell has a way of making one feel dumb. This is by far the most
> challenging programming language I've ever used.
i wouldn't see either as a particularly positive attribute. a language should enable,
explain, encourage, and perhaps inspire. if it also leaves you feeling that you could
do more and go further should you ever want or need to, that's okay, too.
but (unless designed for such purpose;) a language should not be a puzzle, nor
an obstacle, nor should its users feel judged or needlessly challenged.
of course, all of that also depends on not only on the language, but on how it is
used, presented, and perceived. haskell allows for a wide variety of approaches
to programming, allowing programmers to be productive in plain old functional
programming, or perhaps seemingly less so in advanced pointless type-level
meta-programming. that doesn't mean that either approach is better or worse
than the other, or that one approach fits all problems, or programmers. even
one's measures of what constitutes simple code can evolve over time.
so, by picking those techniques that work for you, and keeping an eye open for
those that you're not (yet) comfortable with, you seem to be on the right track.
perhaps other techniques will make more sense to you later, or you'll find the
need to overcome limitations in the techniques you use now (which is how most
of the more complicated-looking themes have developed from easier-looking
ones in the past), perhaps not. understanding more advanced haskell tricks can
be fun, but for most programmers, the point is not (just:) to engage in intellectual
games and challenges for their own sake, but in exploring what their current
understanding of haskell does or does not allow them to do.
> I won't try to understand fix just yet, but I'm still confused by the type of fix:
> fix :: (a -> a) -> a
> It appears to me that it takes a function as an argument, and that
> function takes a single argument. So how are you passing fix an
> anonymous function taking 2 arguments? Sorry if I have beaten this
> horse to death, but my pea-sized brain is working overtime here.
fix takes a function as an argument, and that function takes a single argument.
that function also returns something of the same type as its single argument.
which is where things get more interesting/complicated. but first:
lets give the argument of fix a name and its type: f :: a -> a
so fix takes f and produces an a, but where can that a come from?
that a could be the result of f, but then we'd need something of type a to put
into f. so, once again, we need an a. where could this a come from (i'm not a
language, so i feel free to pose challenging puzzles;-)?
hint: the relevant equation for fix is
fix f = f (fix f) -- (1)
read from right to left, we can see that f applied to (fix f) returns (fix f), which
is why (fix f) is called a fixed point of f (and fix the fixed-point combinator).
read from left to right, we can see how fix accomplishes the feat of computing
a fixed point for any function f :: a -> a we might pass in as a parameter - it
applies f to the fixed point yet to be computed, which unfolds to:
fix f = f (f (..... f (fix f)..)..)
so, f gets applied to a copy of itself, applied to a copy of itself, applied to ..
in other words, fix f recursively creates a supply of copies of f, which f can
make use of in its definition. lets say f = \self-> .. self .., then
fix (\self-> .. self ..)
= (\self-> .. self ..) (fix (\self-> .. self ..)) -- by (1)
= .. (fix (\self-> .. self ..)) .. -- reduce and substitute for self
= .. ((\self-> .. self ..) (fix (\self-> .. self ..))) .. -- by (1)
so, fix f supplies f with copies to use for recursive calls within its body, and
that a type is really the type of the body of f, as well as that of the self-reference.
which brings us to that extra challenge of extra parameters. so far, f is a function
only so that its body can take in a self-reference, and fix f corresponds to a
recursive variable definition. if we want to define a recursive function instead,
we need more parameters. lets say f = \self-> \x-> .. (self x) .., then
fix (\self->\x-> .. (self x) ..)
= (\self->\x-> .. (self x) ..) (fix (\self->\x-> .. (self x) ..))
= \x->.. ((fix (\self->\x-> .. (self x)..)) x) ..
= \x->.. (((\self->\x-> .. (self x)..) (fix (\self->\x-> .. (self x) ..))) x)..
(it really helps to perform the reductions yourself, rather than read them, btw)
and what has happened to the type of f? f takes a self-reference, and returns a
function of one argument, so
f :: ?? -> (b->c)
and the type of the self-reference is the type of f's body, so
f :: (b->c) -> (b->c)
f :: (b->c) -> b -> c
from which we can see that the type of fix gets instantiated to
fix :: ((b -> c) -> (b -> c)) -> (b -> c)
fix :: ((b -> c) -> (b -> c)) -> b -> c
and suddenly, fix does have two parameters, which flip can flip!-)
no magic, just technology sufficiently advanced to be indistinguishable from it:
a function of one parameter, which returns a function of one parameter, is a
function of more than one parameter.
at which point this particular fixed-point combinator puts its recursive
unfoldings to rest for tonight.
More information about the Haskell-Cafe