CSE 505 Lecture Notes - 26 Sept 1994 - Functional Programming Miranda is a good example of a modern functional programming language; we have a nice implementation available We'll also look briefly at a more recent language, Haskell imperative languages: variables, assignment von Neumann machine no referential transparency in mathematics, can substitute equals for equals if x=y+z then you can substitute y+z for x interesting stuff about Miranda truly functional lazy evaluation can deal with infinite structures type system -- statically typed, no declarations needed polymorphic implementation combinators other functional languages FP ML, SML Haskell Hope not Lisp!! future of functional languages ...? barriers: efficiency (declining -- parallelism) programmer acceptance unnatural for some things -- interactive I/O a mess write some things functionally important to know about in any case to run Miranda: lynx% mira or lynx% mira myprog.m to load file "myprog.m" interactive environment (like Lisp) Miranda 3+4 7 Miranda 8 : [1..10] [8,1,2,3,4,5,6,7,8,9,10] Miranda [1,2,3] ++ [8,9] [1,2,3,8,9] Miranda member [1,2,3] 8 False defining a function double x = x+x ---------- recursion examples: || recursive factorial rec_factorial n = 1, n=0 = n * rec_factorial (n-1), n>0 || alternate version using pattern matching: pattern_factorial 0 = 1 pattern_factorial (k+1) = (k+1) * pattern_factorial k || factorial using the built-in prod function factorial n = product [1..n] || mapping function, like mapcar in Lisp my_map f [] = [] my_map f (a:x) = f a : my_map f x ------------------------------ factorial 3 my_map factorial [1,5] my_map factorial [1..10] my_map factorial [1..] ------------------------------ /man - online manual /help /edit - edit your program; reload on exit from editor /quit (or control d) see also ~borning/miranda/* on lynx all the examples in these lecture notes are on lecture.m Miranda is quite terse data types: numbers (both int and float) 3 4.5 3.9e10 booleans: True False characters: 'a' lists: [1,2,3,8] type system requires that all elements must be of the same type [2,4,7] [[1,2],[4,9]] strings -- shorthand for list of chars "hi there" tuples: (False,[1,2,3],"str") functions are first class citizens function application denoted by juxtaposition neg 3 -3 member [1,4,6] 4 infix -- 3+4 (+) 3 4 arithmetic + - * / sin cos etc sum, product product [1,2,8] -------------------- Currying! plus x y = x+y f = plus 1 ff = (+) 1 twice f x = f (f x) g = twice f g 3 ------------------------------ block structure - lexical scoping hyp x y = sqrt sum where sum = x*x + y*y hyp2 x y = sqrt sum where sum = x2 + y2 where x2 = x*x y2= y*y ---------- lazy evaluation and infinite lists my_if True x y = x my_if False x y = y my_if (x>y) 3 4 my_if (x>y) 3 (1/0) ones = 1 : ones compare with circular list in Lisp: (setf ones '(1)) (nconc ones ones) ints n = n : ints (n+1) [1..] lets = "abc" ++ lets two prime number programs: factors n = [k | k <- [1..n]; n mod k = 0] dullprimes = filter isprime [2..] where isprime p = (factors p = [1,p]) interestingprimes = sieve [2..] where sieve (p:x) = p : sieve [n | n <- x; n mod p > 0] -------------------- Hamming numbers: my_merge (x:a) (y:b) = x : my_merge a (y:b) , x num + :: num -> num -> num (note that -> is right associative) member:: [*] -> * -> bool = :: * -> * -> bool run time error if = applied to functions type system not always as descriptive as one would like for append and member, just right, but situation becomes impossible for object-oriented languages: want a type system that specifies that a variable x can hold any object that understands the + and * messages ... how does type checking work? variables are like the logic variables we'll encounter in Prolog unification -- two-way pattern matching double x = x+x map f [] = [] map f (a:x) = f a: map f x map::(*->**)->[*]->[**] map code "0123" [48,49,50,51] twice:: (*->*)->*->* map f [] = [] map f (a:x) = f a: map f x foldr::(*->**->**)->**->[*]->** foldr op r = f where f [] = r f (a:x) = op a(f x) sum = foldr (+) 0 fix:: (*->*) -> * -------------------- User-defined types numtree ::= EmptyNumTree | NumNode num numtree numtree t1 = NumNode 7 EmptyNumTree (NumNode 4 EmptyNumTree EmptyNumTree) || polymorphic types: tree * ::= EmptyTree | Node * (tree *) (tree *) t2 = Node 7 EmptyTree (Node 4 EmptyTree EmptyTree) flatten (Node n left right) = flatten left ++ [n] ++ flatten right flatten EmptyTree = [] allow enumeration types, union types, variant records weekday ::= Mon | Tues | Wed | Thurs | Fri intbool ::= I num | B bool string == [char] token ::= Coeff num | Var string | Exp | Plus | Times -------------------- ZF notation (list comprehensions) a list of cubes of numbers betwen 1 and 10 [ n*n*n | n <- [1..100] ] all cubes: [ n*n*n | n <- [1..] ] perms [] = [[]] perms x = [ a:y | a <- x; y <- perms (x--[a]) ] || quicksort qsort [] = [] qsort (a:x) = qsort [ b | b <- x; b<=a ] ++ [a] ++ qsort [ b | b <- x; b>a ] || square root `limit' applied to a list of values, returns the first value which is the same as its successor. Useful in testing for convergence. For example the following Miranda expression computes the square root of r by the Newton-Raphson method sqt r = limit [x | x<-r, 0.5*(x + r/x).. ] > limit::[*]->* > limit (a:b:x) = a, if a=b > = limit (b:x), otherwise > limit other = error "incorrect use of limit" cp x y = [ (a,b) | a <- x; b <- y] Miranda cp [1..5] [1..4] [(1,1),(1,2),(1,3),(1,4),(2,1),(2,2),(2,3),(2,4), (3,1),(3,2),(3,3),(3,4),(4,1),(4,2),(4,3),(4,4),(5,1),(5,2),(5,3),(5,4)] cpdiag x y = [ (a,b) // a <- x; b <- y] rats = cpdiag [1..] [1..] [(1,1),(1,2),(2,1),(1,3),(2,2),(3,1),(1,4),(2,3),(3,2),(4,1), (1,5),(2,4),(3,3),(4,2),(5,1),(1,6),(2,5),(3,4),(4,3),(5,2), (6,1),(1,7),(2,6),(3,5),(4,4),(5,3),(6,2),(7,1),(1,8),(2,7), (3,6),(4,5),(5,4),(6,3),(7,2),(8,1),(1,9),(2,8),(3,7),(4,6),(5,5), (6,4),(7,3),(8,2),(9,1),(1,10),(2,9),(3,8),(4,7),(5,6),(6,5), ... -------------------- force -- forces all parts of its argument to be evaluated other functional languages that don't use combinators -- more conventional approach to lazy evaluation would be to pass a thunk there, strictness analysis can win big in efficiency -------------------- Abstract data types in Miranda abstype queue * with emptyq :: queue * isempty :: queue * -> bool head :: queue * -> * add :: * -> queue * -> queue * remove :: queue * -> queue * queue * == [*] emptyq = [] isempty [] = True isempty (a:x) = False head (a:x) = a add a q = q ++ [a] remove (a:x) = x outside of the definition, the only way you can get at the queue is via its protocol. Example: building a queue q1 = add 5 (add 3 emptyq) q2 = remove q1 this won't work: q1 = [3,5]