<<

Functional vs. Imperative

Imperative programming concerned with “how.” The main (good) property of is Functional Programming referential transparency. Functional programming concerned with “what.” COMS W4115 Every expression denotes a single value. Based on the of the Prof. Stephen A. Edwards (Church as opposed to Turing). The value cannot be changed by evaluating an expression Spring 2003 or by sharing it between different parts of the program. “Programming without variables” Columbia University No references to global data; there is no global data. Department of Science It is inherently concise, elegant, and difficult to create subtle bugs in. There are no side-effects, unlike in referentially opaque Original version by Prof. Simon Parsons languages. It’s a cult: once you catch the functional bug, you never escape.

The Joy of Pascal Strange behavior Variables program example(output) This prints 5 then 4. At the heart of the “problem” is fact that the global data var flag: boolean; flag affects the value of f. Odd since you expect f(n:int): int In particular begin f (1) + f (2) = f (2) + f (1) if flag then f := n flag := not flag else f := 2*n; gives the offending behavior flag := not flag Mathematical functions only depend on their inputs end They have no memory Eliminating assignments eliminates such problems. begin In functional languages, variables not names for storage. flag := true; What does this print? Instead, they’re names that refer to particular values. writeln(f(1) + f(2)); writeln(f(2) + f(1)); Think of them as not very variables. end

Simple functional programming in A more complex function Currying ML - fun max a b = Functions are first-class objects that can be manipulated A function that squares numbers: = if a > b then a else b; with abandon and treated just like numbers. val max = fn : int -> int -> int % sml - max 10 5; - fun max a b = if a > b then a else b; Standard ML of New Jersey, Version 110.0.7 val it = 10 : int val max = fn : int -> int -> int - fun square x = x * x; - max 5 10; - val max5 = max 5; val square = fn : int -> int val it = 10 : int val max5 = fn : int -> int - square 5; - - max5 4; val it = 25 : int val it = 5 : int Notice the odd type: - - max5 6; int -> int -> int val it = 6 : int This is a function that takes an integer and returns a - function that takes a function and returns an integer. Tuples and arguments Polymorphism Recursion

You can also pass and return tuples to/from functions: Reverse has an interesting type: ML doesn’ have variables in the traditional sense, so you can’t write programs with loops. - fun max(a,b) = if a > b then a else b; - fun reverse(a,b) = (b,a); val max = fn : int * int -> int val reverse = fn : ’a * ’b -> ’b * ’a So use recursion: - max (10,5); - fun sum n = This means it can reverse a two-tuple of any type and any val it = 10 : int = if n = 0 then 0 else sum(n-1) + n; other type: - fun reverse(a,b) = (b,a); val sum = fn : int -> int val reverse = fn : ’a * ’b -> ’b * ’a - reverse (10,5.2); - sum 2; - reverse (10,5); val it = (5.2,10) : real * int val it = 3 : int val it = (5,10) : int * int - reverse ("foo", 3.14159); - sum 3; - max (reverse (10,5)); val it = (3.14159,"foo") : real * string val it = 6 : int val it = 10 : int - sum 4; - val it = 10 : int

Power operator Using the same thing twice The let expression

You can also define functions as infix operators: Without variables, duplication may appear necessary: The easiest way is to introduce a local name for the thing we need multiple times: - fun x ˆ y = fun f x = = if y = 0 then 1 g(square(max(x,4))) + fun f x = = else x * (x ˆ (y-1)); (if x <= 1 then 1 let val ˆ = fn : int * int -> int else g(square(max(x,4)))); val gg = g(square(max(x,4))) - 2 ˆ 2; in One fix: use an extra function: val it = 4 : int gg + (if x <=1 then 1 else gg) - 2 ˆ 3; fun f1(a,b) = b + (if a <= 1 then 1 else b); end; val it = 8 : int fun f x = f1(x, g(square(max(x,4)))); -

let is not assignment Data Types Lists

- let val a = 5 in Programs aren’t very useful if they only manipulate Tuples have parenthesis, lists have brackets: = (let scalars. - (5,3); = val a = a + 2 Functional languages are particularly good at val it = (5,3) : int * int = in manipulating more complex data types. - [5,3]; = a val it = [5,3] : int list = end, You’ve already seen tuples, which is a fixed-length list of = a) specific types of things. Concatenate lists with @: = end; ML also has lists, arbitrary-length vectors of things of the - [1,2] @ [3,4,5]; val it = (7,5) : int * int same type. val it = [1,2,3,4,5] : int list Cons Other list functions Fun with recursion

Add things to the front with :: (pronnounced “cons”) - null [1,2]; - fun addto (l,v) = val it = false : bool - [1,2,3]; = if null l then nil - null nil; val it = [1,2,3] : int list = else hd l + v :: addto(tl l, v); val it = true : bool - 5 :: it; val addto = fn : int list * int -> int list - null []; val it = [5,1,2,3] : int list val it = true : bool - addto([1,2,3],2); Concatenating is not the same as consing: - val a = [1,2,3,4]; val it = [3,4,5] : int list - [1,2] :: [3,4]; val a = [1,2,3,4] : int list stdIn: Error: operator and operand don’t agree [literal]- hd a; operator domain: int list * int list list val it = 1 : int operand: int list * int list - tl a; in expression: val it = [2,3,4] : int list (1 :: 2 :: nil) :: 3 :: 4 :: nil

More recursive fun But why always name functions? Recursion with Lambda Expressions - fun map (f, l) = - map( fn x => x + 5, [10,11,12]); = if null l then nil val it = [15,16,17] : int list Q: How do you call something recursively if it doesn’t have = else f (hd l) :: map(f, tl l); a name? This is called a lambda expression: it’s simply an val map = fn : (’a -> ’b) * ’a list -> ’b list unnamed function. A: Give it one.

- fun add5 x = x + 5; The fun operator is similar to a lambda expression: - let = val rec f = val add5 = fn : int -> int - val add5 = fn x => x + 5; = fn x => if null x then nil val add5 = fn : int -> int = else hd x + 1 :: f (tl x) - map(add5, [10,11,12]); - add5 10; = in f end val it = [15,16,17] : int list val it = 15 : int = [1,2,3]; val it = [2,3,4] : int list

Pattern Matching Pattern Matching Call-by-need

Functions are often defined over ranges More fancy binding Most imperative languages, as well as ML, use call-by-value or call-by-reference. All arguments are x if x ≥ 0 fun map (_,[]) = [] f (x) = evaluated before being passed (copied) to the callee. −x otherwise. | map (f,h :: t) = f h :: map(f,t); But some functional languages use call-by-need. Functions in ML are no different. How to cleverly avoid “_” matches anything - fun infinite x = infinite x; writing if-then: h :: t matchs a list, binding h to the head and t to the val infinite = fn : ’a -> ’b fun map (f,[]) = [] tail. - fun zero _ = 0; | map (f,l) = f (hd l) :: map(f,tl l); val zero = fn : ’a -> int Pattern matching is order-sensitive. This gives an error. - zero (infinite 2); fun map (f,l) = f (hd l) :: map(f,tl l) This doesn’t terminate in ML, but it does with call-by-need. | map (f,[]) = []; Call-by-need deterministic in the absence of side effects. Reduce Another Example Call-by-need

Another popular functional language construct: Consider fun find1(a,b) = if b then true else (a = 1); fun reduce (f, z, nil) = z - fun find1(a,b) = Call-by-value: | reduce (f, z, h::t) = f(h, reduce(f, z, t)); = if b then true else (a = 1); reduce(find1, false, [1, 3, 5, 7] val find1 = fn : int * bool -> bool find1(1, find1(3, find1(5, find1(7, false)))) f reduce(f,z,a::b::) If is “−”, is a − (b − (c − z)) find1(1, find1(3, find1(5, false))) - reduce( fn (x,y) => x - y, 0, [1,5]); - reduce(find1, false, [3,3,3]); find1(1, find1(3, false)) find1(1, false) val it = ˜4 : int val it = false : bool true - reduce( fn (x,y) => x - y, 2, [10,2,1]); val it = 7 : int - reduce(find1, false, [5,1,2]); Call-by-need: val it = true : bool reduce(find1, false, [1, 3, 5, 7] find1(1, find1(3, find1(5, find1(7, false)))) true

The Lambda Calculus Bound and Unbound Variables Arguments

Fancy name for rules about how to represent and evaluate In λx . ∗ 2 x, x is a bound variable. Think of it as a formal λx . λy . ∗ (+ x y) 2 expressions with unnamed functions. parameter to a function. is equivalent to the ML Theoretical underpinning of functional languages. “∗ 2 x” is the body. fn x => fn y => (x + y) * 2; Side-effect free. The body can be any valid lambda expression, including All lambda calculus functions have a single argument. Very different from the Turing model of a store with another unnnamed function. evolving . As in ML, multiple-argument functions can be built through λ . λ . such “currying.” ML: The Lambda Calculus: x y ∗ (+ x y) 2 fn x => 2 * x; λx . ∗ 2 x In this context, currying has nothing to do with Indian “The function of x that returns the function of y that returns cooking. It is due to Haskell Brooks Curry (1900–1982), English: the product of the sum of x and y and 2.” who contributed to the theory of functional programming. “the function of x that returns the product of two and x” The Haskell functional language is named after him.

Calling Lambda Functions Grammar of Lambda Expressions Evaluating Lambda Expressions

To invoke a Lambda function, we place it in parentheses Utterly trivial: Pure lambda calculus has no built-in functions; we’ll be before its argument. impure. expr → constant λ . Thus, calling x ∗ 2 x with 4 is written | variable To evaluate (+ (∗ 5 6) (∗ 8 3)), we can’t start with + because it only operates on numbers. (λx . ∗ 2) 4 | expr expr There are two reducible expressions: 5 6 and 8 3 . This means 8. | (expr) (∗ ) (∗ ) We can reduce either one first. For example: λ Curried functions need more parentheses: | variable . expr (+ (∗ 5 6) (∗ 8 3)) (λx . (λy . ∗ (+ x y) 2) 4) 5 Somebody asked whether a language needs to have a large syntax to be powerful. Clearly, the answer is a (+ 30 (∗ 8 3)) This binds 4 to y, 5 to x, and means 18. Looks like deriving a resounding “no.” (+ 30 24) sentence from a grammar. 54 Evaluating Lambda Expressions Beta-reduction More Beta-reduction

We need a reduction rule to handle λs: May have to be repeated: Repeated names can be tricky: λ . λ . (λx . ∗ 2 x) 4 (( x ( y − x y)) 5) 4 (λx . (λx . + (− x 1)) x 3) 9 λ . (∗ 2 4) ( y − 5 y) 4 (λx . + (− x 1)) 9 3 8 (− 5 4) + (− 9 1)) 3 This is called β-reduction. 1 + 8 3 Functions may be arguments: The formal parameter may be used several times: 11 (λ f . f 3) In the first line, the inner belongs to the inner λ, the outer (λx . + x x) 4 x (λx . + x 1)3 x belongs to the outer one. (+ 4 4) (+ 3 1) 8 4

Free and Bound Variables Alpha conversion Alpha Conversion

In an expression, each appearance of a variable is either One way to confuse yourself less is to do α-conversion. An easier way to attack the earlier example: “free” (unconnected to a λ) or bound (an argument of a λ). This is renaming a λ argument and its bound variables. (λx . (λx . + (− x 1)) x 3) 9 β-reduction of λx . E y replaces every x that occurs free ( ) Formal parameters are only names: they are correct if (λx . (λy . + (− y 1)) x 3) 9 in E with y. they are consistent. (λx . + (− x 1)) 9 3 Free or bound is a function of the position of each variable λx . (λx . x) (+ 1 x) ↔α λx . (λy . y) (+ 1 x) + (− 9 1)) 3 and its context. + 8 3 Free variables 11 (λx . x y (λy . + y )) x Bound variables

Reduction Order Reduction Order Reduction Order

The order in which you reduce things can matter. Reducing (λz . z z) (λz . z z) effectively does nothing The redex is a sub-expression that can be reduced. because λz . z z is the function that calls its first argument (λx . λy . y) ( (λz . z z) (λz . z z) ) ( ) The leftmost redex is the one whose λ is to the left of all on its first argument. The expression reduces to itself: other redexes. You can guess which is the rightmost. We could choose to reduce one of two things, either λ . λ . ( z z z) ( z z z) The outermost redex is not contained in any other. (λz . z z) (λz . z z) So always reducing it does not terminate. The innermost redex does not contain any other. or the whole thing However, reducing the outermost function does terminate λ . λ . λ . λ . λ . λ . λ . λ . For ( x y y) ( ( z z z) ( z z z) ), ( x y y) ( ( z z z) ( z z z) ) because it ignores its (nasty) argument: (λz . z z) (λz . z z) is the leftmost innermost and (λx . λy . y) ( (λz . z z) (λz . z z) ) (λx . λy . y) ( (λz . z z) (λz . z z) ) is the leftmost outermost. λy . y Applicative vs. Normal Order Applicative vs. Normal Order Normal Form

Applicative order reduction: Always reduce the leftmost Applicative: reduce leftmost innermost A lambda expression that cannot be reduced further is in innermost redex. normal form. “evaluate arguments before the function itself” Normative order reduction: Always reduce the leftmost Thus, eager evaluation, call-by-value, usually more efficient outermost redex. λy . y For λx . λy . y λz . z z λz . z z , applicative order ( ) ( ( ) ( ) ) is the normal form of reduction never terminated but normative order did. Normative: reduce leftmost outermost (λx . λy . y) ( (λz . z z) (λz . z z) ) “evaluate the function before its arguments” , call-by-name, more costly to implement, accepts a larger class of programs

Normal Form The Church-Rosser Theorems Church-Rosser

Not everything has a normal form If E1 ↔ E2 (are interconvertable), then there exists an E Amazing result: such that E → E and E → E. (λz . z z) (λz . z z) 1 2 Any way you choose to evaluate a lambda expression will “Reduction in any way can eventually produce the same produce the same result. can only be reduced to itself, so it never produces an result.” non-reducible expression. Each program means exactly one thing: its normal form. “Infinite loop.” The lambda calculus is deterministic w..t. the final result.

If E1 → E2, and E2 is is normal form, then there is a Normal order reduction is the most general. normal-order reduction of E1 to E2. “Normal-order reduction will always produce a normal form, if one exists.”

Turing Machines vs. Lambda Calculus

In 1936,

• Alan Turing invented the Turing machine

• Alonzo Church invented the lambda calculus

In 1937, Turing proved that the two models were equivalent, i.e., that they define the same class of computable functions. Modern processors are just overblown Turing machines. Functional languages are just the lambda calculus with a more palatable syntax.