400 likes | 547 Views
CS5205: Foundation in Programming Languages. Basics of Functional Programming. Topics. Higher-Order Functions Formal Reasoning Abstraction vs Efficiency Bridging the Divide. Function Abstraction.
E N D
CS5205: Foundation in Programming Languages Basics of Functional Programming. Haskell
Topics • Higher-Order Functions • Formal Reasoning • Abstraction vs Efficiency • Bridging the Divide Haskell
Function Abstraction • Function abstraction is the ability to convert any expression into a function that is evaluated at a later time. <Expr> p = \ () -> Expr time p () time Normal Execution Delayed Execution Haskell
Higher-Order Functions • Higher-order programming treats functions as first-class, allowing them to be passed as parameters, returned as results or stored into data structures. • This concept supports generic coding, and allows programming to be carried out at a more abstract level. • Genericity can be applied to a function by letting specific operation/value in the function body to become parameters. Haskell
foldr f u ls = case ls of [] -> u x:xs -> f x(foldr f u xs) Genericity • Replace specific entities (0 and +) by parameters. sumList ls = case ls of [] -> 0 x:xs -> x+(sumList xs) Haskell
foldr :: (a -> b -> b) -> b -> [a] -> b Polymorphic, Higher-Order Types sumList :: [Int] -> Int sumList :: Num a => [a] -> a Haskell
Instantiating Generic Functions sumL2 :: Num a => [a] -> a sumL2 ls = foldr (+)0 ls sumL2 [1, 2, 3] ) sumL2 [1.1, 3, 2.3] ) Haskell
Instantiating Generic Functions prodL :: Num a => [a] -> a prodL ls = foldr (*)1 ls prodL [1, 2, 5] ) prodL [1.1, 3, 2.3] ) Haskell
Instantiating Generic Functions • Can you express map in terms of foldr? map :: (a -> b) -> [a] -> [b] map f [] = [] map f (x:xs) = (f x) : (map f xs) map f xs = foldr … … … xs Haskell
Can we express filter in terms of foldr? filter f xs = foldr … … … xs Instantiating Generic Functions • Filtering a list of elements with a predicate. filter :: (a -> Bool) -> [a] -> [a] filter f [] = [] filter f (x:xs) = if (f x) then x : (filter f xs) else filter f xs Haskell
Similar to Unix pipe command: cmd1 | cmd2 Pipe/Compose compose :: (b -> c) -> (a -> b) -> a -> c compose f g = \ x -> f (g x) g | f = compose f g Haskell
In Haskell, type class help give a more generic type: for :: Num b, Ord b => b -> b -> (b -> a -> a) -> a -> a Iterator Construct for :: Int -> Int -> (Int -> a -> a) -> a -> a for i j f a = if i>j then a else for (i+1) j (f i a) Haskell
associate to right Right Folding foldr f u [x1,x2,..,xn] • f x1 (foldr f u [x2 ..xn]) • f x1 (f x2 (fold f u [x3..xn])) • f x1 (f x2 (… (fold f u [xn]) …)) • f x1 (f x2 (… (f xn u) …))) Haskell
Left Folding – Tail Recursion • Accumulate result in a parameter: foldl f u ls = case ls of [] -> u x:xs -> foldl f (f u x) xs based on accumulation • What is the type of foldl? • Can we compute factorial using it? Haskell
left is here! Left Folding foldl f u [x1,x2,..,xn] • foldl f (f u x1) [x2 ..xn] • foldl f (f (f u x1) x2) [x3..xn])) • foldl f (f … (f (f u x1) x2)… xn) [] • f (… (f (f u x1) x2) …) xn Haskell
sumList ls = sumT 0 ls sumT acc ls = foldl (+) acc ls Instance of Left Folding • Summing a list by accumulation. sumT acc ls = case ls of [] -> 0 x:xs -> sumT (x+acc) xs Haskell
Referential Transparency • An expression is referentially transparent if it can always be replaced by an equivalent expression with the same value and effect. Allows reasoning based on components. • Useful for: • simplifying algorithm • proving correctness • optimization + parallelization • Pure functions are referentially transparent, as relied on in mathematical reasoning. Haskell
Equivalence Proof • Can we Prove : sumList xs = sumT 0 xs. • Generalise : (sumList xs)+a = sumT a xs. • By Induction • Case : x=[] • (sumList [])+a = sumT a [] • 0+a = a • Case : x=x:xs • (sumList x:xs)+a = sumT a (x:xs) • x+(sumList xs)+a = sumT (x+a) xs • (sumList xs)+(x+a) = sumT (x+a) xs • // apply induction hypothesis Haskell
rev xs = foldr … … … What is the time complexity? List Reversal • Concatenate first element to last position. rev [] = [] rev (x:xs) = rev xs ++ [x] Haskell
Time Complexity • Assume : C(xs++ys) = length xs • Derive :Steps(rev(xs)) • Case [] : • Steps(rev([])) = 1+Steps([]) • = 1+0 • Case x:xs : • Steps(rev(x:xs)) • = 1+Steps(rev(xs)++[a]) • = 1+C(rev(xs)++_)+Steps(rev(xs)) • = 1+length(rev(xs)+Steps(rev(xs)) • = 1+length(xs)+Steps(rev(xs)) • Thus : C(rev(xs)) = (length xs)^2 Haskell
Same as: revT w xs = foldl (\ w x -> x:w) w xs What is the time complexity? Iterative List Reversal • Concatenate first element to last position. revT w [] = w revT w (x:xs) = revT (x:w) xs Haskell
Time Complexity • DeriveSteps(revT w xs) • Case [] : • Steps(revT w []) = 1+Steps(w) • = 1+0 • Case x:xs : • Steps(revT w (x:xs)) • = 1+Steps(revT (x:w) xs) • = 1+Steps(revT _ xs) • Thus : C(revT w xs) = (length xs) Haskell
Abstraction vs Efficiency • Abstraction helps with programmers’ productivity • Efficiency helps machine execution. • Tension between abstraction and efficiency • Abstract program • stress on ‘what’ rather than ‘how’ • typically uses simpler (maybe naïve) algorithm • Efficient program • optimised implementation • use of clever programming techniques Haskell
Bridging the Divide Abstract Code/Specs transform or synthesize verify Efficient Code or Implementation Haskell
Unfold/Fold Transformation • DEFINE - new function definition • UNFOLD – replace a call by its body • FOLD – replace an expression matching the RHS of a definition by its corresponding call • INSTANTIATE – provide special cases of a given equation. • ABSTRACT – introduce a tuple of expressions • LAW – application of valid lemma, e.g. associativity Haskell
Fusion Transformation • Consider: • …sum (double xs)… • sum [] = 0 • sum x:xs = x+(sum xs) • double [] = [] • double x:xs = 2*x : (double xs) • Computation reuses smaller functions to build larger ones but may result in unnecessary intermediate structures. They can cause space overheads. • Solution : Fuse the code together! Haskell
Fusion Transformation • Define: • sumdb xs = sum (double xs) • Instantiate: xs=[] • sumdb [] = sum (double []) • = sum [] • = 0 • Instantiate: xs=x:xs • sumdb x:xs = sum (double x:xs) • = sum (2*x : double xs) • = 2*x + sum(double xs) • = 2*x + (sumdb xs) Haskell
Iteration Transformation • Define: • sumdbT a xs = a+(sumdb xs) • Instantiate: xs=[] • sumdbT a [] = a+(sumdb []) • = a • Instantiate: xs=x:xs • sumdbT a (x:xs) = a+(sumdb x:xs) • = a+(2*x + sumdb xs) • = (a+2*x) + (sumdb xs) • = sumdbT (a+2*x) xs Haskell
Laziness vs Strictness • Laziness increase expressiveness, allowing infinite data structures, by not evaluating each subexpression until it is really needed. • However, there is a performance penalty (both space and time), as the suspended computation has to be stored as a closure and then invoked subsequently. • If you always need some of the parameters, we might as well evaluate their corresponding arguments first. • Question : When is an argument to a function needed (strict)? Using ? to denote non-termination. f … ? … = ? Haskell
Strictness Transformation • Can analyse that accumulating argument of sumdb is strict. We can force strictness using the `seq` operator. • sumdbT a [] = a • sumdbT a (x:xs) = sumdbT (a+2*x) xs • sumdbT a [] = a • sumdbT a (x:xs) = let p = a+2*x in • p `seq` sumdbT p xs Haskell
Tupling Transformation • Average of a List : • average xs = sum xs / length xs • Define : • both xs = (sum xs, length xs) • Instantiate : • both [] = (sum [], length []) • = (0,0) • Instantiate : • both x:xs = (sum x:xs, length x:xs) • = (x+sum xs, 1+length xs) • = let (u,v)= (sum xs,length xs) in • (x+u, 1+v) • = let (u,v)= both xs in u/v Haskell
Naive Fibonacci • Natural but inefficient version of fibonacci : • fib 0 = 1 • fib 1 = 1 • fib n = (fib n-1)+(fib n-2) • Time complexity is exponential time due to presence of redundant calls. Haskell
A Call Tree of fib fib 6 fib 4 fib 5 fib 4 fib 3 fib 3 fib 2 fib 3 fib 2 fib 2 fib 1 fib 2 fib 1 Many repeated calls! Haskell
A Call Graph of fib fib 6 fib 5 fib 4 fib 3 fib 2 fib 1 No repeated call through reuse of identical calls Haskell
Tupling - Computing Two Results Compute two calls from next two calls in hierarchy: ((fib n) , (fib n-1)) ((fib n-1) , (fib n-2)) Haskell
Tupling Fibonacci • Define : • fibtup n = (fib n+1, fib n) • Instantiate : • fibtup 0 = (fib 1, fib 0) • = (1,1) • Instantiate : • fibtup n+1 = (fib n+2, fib n+1) • = ((fib n+1)+(fib n), fib n+1) • = let (u,v)= (fib n+1, fib n) • in (u+v, u) • = let (u,v)= fibtup n in (u+v, u) Haskell
Using the Tupled Function • fib 0 = 1 • fib 1 = 1 • fib n = fib n-1 + fib n-2 • = let (u,v) = (fib n-1,fib n-2) • in u+v • = let (u,v) = fibtup (n-2) • in u+v Haskell
Linear Recursion fib 6 fib 5 • fibtup 0 = (1,1) • fibtup (n+1) = • let (u,v)= fibtup n • in (u+v,u) fib 4 fib 3 fib 2 fib 1 Haskell
To Iteration fib 6 fib 5 • fibT 0 u v • = (u,v) • fibT (n+1) u v • = fibT n (u+v,u) fib 4 fib 3 fib 2 fib 1 Haskell
Optimization • Should be done only if critical. • Should be automated where possible, for e,g. as part of compilation. • Manual optimization by human should preferably be carefully checked or verified Haskell