I'm trying to understand something about Haskell functions.
First, here is a Fibonacci function defined in the typical "slow" way (i.e. recursive with no memoization, and no infinite-list tricks)
slowfib :: Int -> Integer
slowfib 0 = 0
slowfib 1 = 1
slowfib n = slowfib (n-2) + slowfib (n-1)
Next, a canonical memoizing version of the same. (Only slightly different from typical examples in tutorals/books/etc, because I prefer the prefix version of the !!
operator.)
memfib = (!!) (map fib [0..])
where
fib 0 = 0
fib 1 = 1
fib k = memfib(k-2) + memfib(k-1)
The above solution uses partial application of the !!
operator, which makes sense: we want memfib
to end up as a function that takes a parameter, and we are defining it without including a parameter in the definition.
So far so good. Now, I thought I could write an equivalent memoizing function that does include a parameter in the definition, so I did this:
memfib_wparam n = ((!!) (map fib [0..])) n
where
fib 0 = 0
fib 1 = 1
fib k = memfib_wparam(k-2) + memfib_wparam(k-1)
(In Lambda calculus terms, memfib
and memfib_wparams
are just eta-conversions of each other. I think???)
This works, but the memoization is gone. In fact, memfib_wparam
behaves even worse than showfib
: Not only is it slower, but its memory usage is more than double.)
*Main> slowfib 30
832040
(1.81 secs, 921,581,768 bytes)
*Main> memfib 30
832040
(0.00 secs, 76,624 bytes)
*Main> memfib_wparam 30
832040
(2.01 secs, 2,498,274,008 bytes)
What's going on here? More importantly, what is my broader understanding of Haskell function definitions getting wrong? I was assuming the syntax I used in memfib_wparam
was just syntactic sugar for what I did in memfib
, but clearly it isn't.
Pattern matching consists of specifying patterns to which some data should conform and then checking to see if it does and deconstructing the data according to those patterns. When defining functions, you can define separate function bodies for different patterns.
From HaskellWiki. A partial function is a function that is not defined for all possible arguments of the specified type. Examples in the Haskell standard library are: head , tail : undefined for empty lists. (!!) : undefined if the index is at least as big as the list length.
The most basic way of defining a function in Haskell is to ``declare'' what it does. For example, we can write: double :: Int -> Int double n = 2*n. Here, the first line specifies the type of the function and the second line tells us how the output of double depends on its input.
Haskell guards are used to test the properties of an expression; it might look like an if-else statement from a beginner's view, but they function very differently. Haskell guards can be simpler and easier to read than pattern matching .
The difference is in when your fib
function is bound.
where
-bound definitions have access to the outer function's parameters (i.e. the parameters are "in scope" within where
). This means that fib
should have access to n
, which in turn means that fib
is defined after n
is passed, which means it's a different fib
for every n
, which means it's a different call to map fib [0..]
for every n
.
If you wanted to eta-expand your memfib
, this would be the "right" way to do it (i.e. without unduly expanding the scope of n
):
memfib = \n -> theCache !! n
where
theCache = map fib [0..]
fib 0 = 0
fib 1 = 1
fib k = memfib(k-2) + memfib(k-1)
If you're comparing with lambda calculus, the key difference is that eta-reduction/expansion doesn't say anything about performance, it just guarantees that the result of the program stays the same logically. Which it does.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With