Martin Sulzmann
Evaluation order
Call-by value, call-by name, …
Haskell applies “lazy” evaluation (implementation method for call-by name)
Higher-order functions
How to compile higher-order functions
From higher-order to first-order
(\x-> ...) e
, arithmetic term such as 1 + 3
etcCall-by value (CBV) = AOR with the exception that we don’t evaluate under “lambda” (i.e. inside function bodies)
Outermost, leftmost, also called Normal Order Reduction (NOR)
square (inc 1) => (inc 1) * (inc 1)
=> (1 + 1) * (inc 1)
=> 2 * (inc 1)
=> 2 * (1 + 1)
=> 2 * 2
=> 4
Call-by name (CBN) = NOR with the exception that we don’t evaluate under “lambda” (i.e. inside function bodies)
FP princple = Write functions by composing existing functions
Recall that .
stands for function
composition
map :: (a -> b) -> [a] -> [b]
map f [] = []
map f (x:xs) = f x : map f xs
or :: [Bool] -> Bool
or [] = False
or (True:xs) = True
or (x:xs) = or xs
any :: (a -> Bool) -> [a] -> Bool
any p = or . map p
map :: (a -> b) -> [a] -> [b]
map f [] = []
map f (x:xs) = f x : map f xs
or :: [Bool] -> Bool
or [] = False
or (True:xs) = True
or (x:xs) = or xs
any :: (a -> Bool) -> [a] -> Bool
any p = or . map p
any (\x -> x > 0) [1,2,3,4]
=> or . map (\x -> x > 0) [1,2,3,4]
=> or (map (\x -> x > 0) [1,2,3,4])
=> or ( (1 > 0) : map (\x -> x > 0) [2,3,4])
=> or ( True : map (\x -> x > 0) [2,3,4])
=>* or [True, True, True, True]
=> True
`=>* denotes a sequence of evaluation steps
CBN is more efficient and supports code reuse
Under CBV we we might do some unnecessary work!
To obtain a more efficient version under CBV, we will have to inline the code (no reuse)
Here’s another example.
myElem :: Eq a => a -> [a] -> Bool
myElem x [] = False
myElem x (y:ys)
| x == y = True
| otherwise = myElem x ys
myElem2 :: Eq a => a -> [a] -> Bool
myElem2 x xs = (not . null) (filter (==x) xs)
Under CBN, myElem
and myElem2
are equally
efficient!
myElem2
reuses existing code/functions.
Suppose we are given a list with one million entries
Retrieve the smallest ten entries with doing as little work as possible
The ‘brute-force’ method is to sort the entire list and then select the first 10 entries
Under CBN, the solution is trivial
ones = 1 : ones
nats = nats' [1] ones
nats' _ [x] = [x]
nats' prev (x:xs) = sum prev : nats' (prev++[x]) xs
General principle
CBN allows for highly declarative control structures
-- Haskell supports lazy evaluation.
-- Lots of cool applications but can also bite us!
---------------------------------------------------------------------------------------------
-- Stream, infinite sequence of data (numbers, temperature reading, internet traffic, ...)
-- Simplified stream example.
data Stream a = Cons a (Stream a) deriving Show
-- Compare this to lists.
data List a = ConsL a (List a) | Null
-- Observation:
-- Every value of type List a is finite!
-- How to emulate the stream data type say in a Java like language?
data JavaStr a = ConsS a (() -> JavaStr a)
-- ^^^^^^^^^^^^^
-- suspended computation
-- () -> Str a refers to a parameter-less function that yields a stream
-- thus we can suspend the computation (and emulate lazy evaluation in a strict language)
headS :: Stream a -> a
headS (Cons x _) = x
headStr :: JavaStr a -> a
headStr (ConsS x _) = x
tailS :: Stream a -> Stream a
tailS (Cons x xs) = xs
tailStr :: JavaStr a -> JavaStr a
tailStr (ConsS _ f) = f () -- force the computation, i.e. take one step
succN :: Integer -> Stream Integer
succN n = Cons n (succN (n+1))
-- Natural numbers: 0, 1, ...
nats :: Stream Integer
nats = succN 0
mapS :: (a -> b) -> Stream a -> Stream b
mapS f (Cons x xs) = Cons (f x) (mapS f xs)
-- Negative numbers: -1, -2, ...
negs :: Stream Integer
negs = mapS (\x -> -x) (tailS nats)
-- interleaveS applied to
-- ... x0, x1, ...
-- ... y0, y1, ...
-- yields
-- ... x0,y0,x1,y1, ...
interleaveS :: Stream a -> Stream a -> Stream a
interleaveS (Cons x xs) ys = Cons x (interleaveS ys xs)
-- All positive, negative numbers.
ints :: Stream Integer
ints = interleaveS nats negs
firstN :: Int -> Stream Integer -> [Integer]
firstN n (Cons x xs)
| n < 0 = error "must be positive"
| n == 0 = []
| otherwise = x : firstN (n-1) xs
first5NegNumbers = firstN 5 negs
Lazy evaluation = Call-by need (CBN)
Technique for implementing CBN more efficiently:
As a result, under lazy evaluation, any one redex is evaluated at most once.
Recall
There is no need to evaluate square (inc 1)
on its
own.
Requires manual effort.
Need to suspend a computation and resume the computation (manually).
We can use “lambdas” to suspend the computation,
e.g. f = \x -> comp
.
We “force” a computation via f ()
.
Try out the above
> ghc Sum.hs
> ./Sum
Stack space overflow: current size 8388608 bytes.
Use `+RTS -Ksize -RTS' to increase it.
So possibly due to the recursive call the stack size is too small?
NOTE
There’s no longer a stack space overflow on today’s big machines. But the strict version (coming up) is considerably faster.
“Iterative” version
Try out the above
> ghc Sum.hs
> ./Sum
Stack space overflow: current size 8388608 bytes.
Use `+RTS -Ksize -RTS' to increase it.
msum
and sumAcc
to
a similar C program which uses a for
loop instead of a
recursive call (“tail call elimination”)sumAcc 0 [1,2,3,4] => sumAcc (0+1) [2,3,4]
=> sumAcc (0+1+2) [3,4]
=> sumAcc (0+1+2+3) [4]
=> sumAcc (0+1+2+3+4) []
=> 0+1+2+3+4
-- There's no longer a stack space overflow on today's big machines.
-- But the strict version is considerably faster.
xs = take (10^8) [1..] -- [1..100000000000]
msum [] = 0
msum (x:xs) = x + msum xs
-- main = print (msum xs)
sumAccStrict acc [] = acc
sumAccStrict acc (x:xs) =
let y = acc + x
in y `seq` sumAccStrict y xs
main = print $ sumAccStrict 0 xs
n
is
strictRecall:
where x1
, …, xn
are the formal parameters.
The above can also be written as follows
relying on the lambda abstraction syntax \x -> ...
(see annonymous functions).
In imperative languages, we collectively supply the arguments to a function. In Haskell, we can achieve the same by passing as an argument a pair/tuple.
Both styles are equivalent and its possible to transform a function with a pair argument into a function which expects a chain of arguments (and vice versa of course).
This transformation is known as Currying
The following transformation functions are part of the Prelude.
curry :: ((a, b) -> c) -> a -> b -> c
curry f = \x -> \y -> f (x,y)
uncurry :: (a -> b -> c) -> (a, b) -> c
uncurry f = \(x,y) -> f x y
Express the various “plus” functions in terms of each other using
curry
and uncurry
.
Functions expect a certain number of arguments
Functions are expressions.
So we can obtain new functions/expression by providing a partial set of a function’s arguments.
Consider the following list of examples
What are the types of the above functions?
CPS is a particular style of writing functional programs
A program is in CPS if no function ever returns
Instead every function takes an extra argument (a function called “the continuation”) that it calls with the result
Next
Factorial function written in direct style (i.e. using “return”)
Sample execution
fac 5 = 5 * fac 4
= 5 * (4 * fac 3)
= 5 * (4 * (3 * fac 2))
= 5 * (4 * (3 * (2 * fac 1)))
= 5 * (4 * (3 * (2 * 1)))
= 5 * (4 * (3 * 2))
= 5 * (4 * 6)
= 5 * 24
= 120
Variable n
stored in the call stack.
Only once the call fac (n-1)
returns, we can compute
the result
k
k
facC 5 k = facC 4 (\v4 -> k (5 * v4))
= facC 3 (\v3 -> (\v4 -> k (5 * v4)) (4 * v3))
= facC 2 (\v2 -> (\v3 -> (\v4 -> k (5 * v4)) (4 * v3)) (3 * v2))
= facC 1 (\v1 -> (\v2 -> (\v3 -> (\v4 -> k (5 * v4)) (4 * v3)) (3 * v2)) (2 * v1))
= (\v1 -> (\v2 -> (\v3 -> (\v4 -> k (5 * v4)) (4 * v3)) (3 * v2)) (2 * v1)) 1
= (\v2 -> (\v3 -> (\v4 -> k (5 * v4)) (4 * v3)) (3 * v2)) 2
= (\v3 -> (\v4 -> k (5 * v4)) (4 * v3)) 6
= (\v4 -> k (5 * v4)) 24
= k 120
Ultimately, we must transform a higher-order language into some simpler language which can be executed by the computer hardware.
There are several methods:
Defunctionalization is a global program transformation to turn higher-order programs into first-order order ones.
The basic idea is to
Next, we will explain the basic idea via an example. In practice, we may need to apply additional steps, e.g. closure conversion (lambda lifting).
translates to
apply :: Eq a1 => Arrow a1 a2 -> a1 -> a2
apply = \ f -> \ arg ->
case f of
X1 -> False
X2 -> let x = arg in X3 x
(X3 x2) -> let x3 = arg in X4 x2 x3
(X4 x2 x3) -> let x4=arg in (x2==x4) || (apply x3 x4)
emptyDF = X1
insertDF = X2
resDF = apply (apply (apply insertDF 1) emptyDF) 2
where we assume
apply :: Eq a1 => Arrow a1 a2 -> a1 -> a2
apply = \ f -> \ arg ->
case f of
X1 -> False
X2 -> let x = arg in X3 x
(X3 x2) -> let x3 = arg in X4 x2 x3
(X4 x2 x3) -> let x4=arg in (x2==x4) || (apply x3 x4)
emptyDF = X1
insertDF = X2
resDF = apply (apply (apply insertDF 1) emptyDF) 2
Evaluate the defunctionalized program
Lazy evaluation (one = 1 : one
)
Higher-order functions
Lambda abstractions (\x -> ...
)
Currying (f x y
versus
f (x,y)
)
Partial function application (map (+1)
)
Continuation-passing style (never return)
From higher-order to first-order programs