CS 541 Lecture -*- Outline -*- * Functions, a built-in, first-class type of SML (chapter 5, up to 5.11) ** fn makes functions this is called lambda in Scheme and LISP a block in Smalltalk Used for avoiding redundancy in code (functional abstraction) and for tool building *** examples: ------------------------ fn MAKES FUNCTIONS (CLOSURES) - (fn x => x)("y"); val it = "y" : string - (* LISP style invocation *) ((fn x => hd x) [1,2,3]); val it = 1 : int ----------------- the function (fn x => hd x) is the same as "hd" ------------------- - ((fn (x,y) => 0) ("a","b")); val it = 0 : int - ((fn () => 5)); val it = fn : unit -> int - (fn () => 5)(); val it = 5 : int ------------------------ talk about parenthesization; LISP style seems clearest to me *** applicative order evaluation rule ---------------------------- APPLICATIVE ORDER EVALUATION ((fn x => e1) e2) =def= let val x = e2 in e1 end examples: ((fn z => z * z + 1) 7) = let val z = 7 in z * z + 1 end ((fn (x,y) => x*y + 3) (5,6)) = let val x = 5 and y = 6 in x*y + 3 end = let val (x,y) = (5,6) in x*y + 3 end ---------------------------- so it's the value of e1, in environment where x denotes value of e2 i.e., Val(((fn x => e1) e2), env) = Val(e1, env + {x |-> Val(e2,env)}) Typically in semantics we think of function application as primitive, and consider let to be sugar for the function application. draw picture v----------------\ global env --> [ +: ... ] | | parent __________|__ | * | * | |__|__|_____| | v parameters: z body: z * z + 1 ------------------------------ FUNCTION CLOSURES def: a *closure* is a function together with the environment in which the fn was evaluated. ------------------------------ like salmon, a function returns to the enivronment of its birth when it's called upon ------------------------------ - val inc = let val one = 1 in (fn x => x + one) end; val inc = fn : int -> int - let val one = 2 in inc(3) end; val it = 4 : int ------------------- Note, top-level definition produces new environment at top level *** environment model draw pictures: ______________________ ___________ global env -> | +: *---------------|----->| | | | inc: * | |____|____| |______|_____________| ^ | ^ |---------| | |parent |parent | _____|_____ __________|___ | | | | one: 2 | | | one: 1 | |____________| | |__________| inc(3) | ^ | | _____v___|___ | * | * | |__|__|_____| | v parameters: x body : x + one *** the point the point of all this machinery is to enforce static scoping. Another way to think about it is that this machinery makes it so your function does what you think it does looking at its def ** function sugars revisited *** sugar for simple fun bindings ---------- FUNCTION SUGARS fun fact 0 = 1 | fact n = n * fact(n-1); (* equivalent to *) fun fact x = (case x of 0 => 1 | n => n * fact(n-1)); (* equivalent to *) val rec fact = fn x => (case x of 0 => 1 | n => n * fact(n-1)); ---------- so in semantics, we can ignore "fun" and only consider val bindings, recall we can desugar pattern matches so all function defs look like fun name args = E so sugar is fun name args = E =def= val rec name = fn args => E *** val rec bindings binds names (like fact) to values of expressions, evaluated in an environment that includes bindings for the names. So the values had better be closures, otherwise not defined! draw the following picture v----------------\ global env --> [ fact: -]--\ | | | parent ___v______|__ | * | * | |__|__|_____| | v parameters: x body: (case x of 0 => 1 | n => n * fact(n-1)) ** Functions first-class in SML (fn x => e1) denotes functional abstraction of e1. *** examples ------------------- SOME SYNTACTIC GAMES - (fn x => x); (* identity function *) val it = fn : 'a -> 'a - val id = (fn x => x); val id = fn : 'a -> 'a - fun id x = x; val id = fn : 'a -> 'a - (id 1); val it = 1 : int - id(1); val it = 1 : int - id 1; val it = 1 : int - id; val it = fn : 'a -> 'a ------------------------ Q: how is the following parsed? How to get it to go the other way? ------------------------ - id id 3; ------------------------- Q: how is the following parsed? -------------------------- - val inc = (fn y => y + 1); val inc = fn : int -> int - inc 3 * 4; -------------------------- *** functionals here we start to see how functionals are useful for tool making ------------------- FUNCTIONALS val hdtl = fn lst => hd (tl lst); fun checkleft t = check (left t); --------------------- identify the common parts of this pattern, and pass the changing parts as arguments --------------------- (* compose functional *) fun compose (f,g) x = f (g x); val compose = (fn (f,g) => (fn x => f(g x))); (* type is: (('a -> 'b) * ('c -> 'a)) -> 'c -> 'b *) ------------------ compose is built-in to SML as infix o (lower-case letter "oh") ------------------- val hdtl = compose(hd,tl); --------------------------- Compare with val hdtl = fn ls => compose(hd,tl) ls; Q: what's the type of the above? Might mention the eta rule... ---------------------------- hdtl [1,2,3]; compose(hd,tl) [1,2,3]; compose(hd,compose(tl,tl)) [1,2,3,4]; -------------------- might trace hdtl [1,2,3] using equations if questions Q: how to write checkleft using compose? look at parenthesization of these last two lines... Q: can you define a function B such that B hd tl = hdtl ? note: this really means (B hd) tl look at the equations: want B hd tl = hdtl = compose(hd,tl) so one way to define this is fun B f g = compose(f,g); But now, what good are f and g doing here? That is, if we want to write val B = (C compose) then we must have... B hd tl = (C compose) hd tl = compose(hd,tl) so we can write... fun C compose hd tl = compose(hd,tl); ---------------------- (* THE CURRY FUNCTIONAL *) fun curry compose hd tl = compose(hd,tl); val curry = (fn f => (fn x => (fn y => f(x,y)))); val by2 = ((curry (op *)) 2); (by2 5); val append = op @; (((curry append) [1,2]) [3,4]); -------------------- one use of curried functions is as "partial application" as shown by the function "by2". This prevents writing 2 redundantly in code... Q: can you write a function in C which is a curried multiplication? have to explicitly represent the closure A good use of curried functions is for defining tool makers... ------------------------ (* MAPPING *) fun add_scalar (s:int) [] = [] | add_scalar s (x::ls) = (s+x)::(add_scalar s ls); fun subst_all new old [] = [] | subst_all new old (x::ls) = (if x = old then new else x) :: (subst_all new old ls); fun map f [] = [] | map f (x::ls) = (f x)::(map f ls); fun map f lst = let fun helper [] = [] | helper (x::ls) = (f x)::(helper ls); in helper lst end; ----------------------- the questions to ask when generalizing from examples: what parts change? (make them into arguments) what parts stay the same? --------------------------- FOR YOU TO DO Recall: datatype 'a tree = Lf | Br of 'a * 'a tree * 'a tree; Generalize: fun preorder Lf = [] | preorder (Br(v,t1,t2)) = [v] @ preorder t1 @ preorder t2; fun inc Lf = Lf | inc (Br(v,t1,t2)) = Br(v+1, inc t1, inc t2); ----------------------- now a little physics... ------------------ PHYSICS FOR FUNCTIONAL PROGRAMMERS (* FIELDS ARE LIKE CURRIED FUNCTIONS *) fun gravfield m1 r_vec m2 = let val G = 6.670E~11 (* N*m^2/kg^2 *) fun force r = if r = 0.0 then 0.0 else (~G * m1 * m2) / (r*r) in map force r_vec (* : N list *) end; val earths_field = let val mass_of_earth = 5.96E24 (*kg*) val rad_of_earth = 6.370E9 (*m*) in gravfield mass_of_earth [0.0,0.0,rad_of_earth] end; ------------------------ so physics uses higher-order concepts like curried functions! ------------------------------ TYPES OF CURRIED FUNCTION APPLICATIONS EXPRESSION TYPE gravfield : kg -> (m list -> (kg -> N)) 5.96E24 : kg (gravfield 68.0) : m list -> (kg -> N) [0.0,0.0,6.0E6] : m list ((gravfield 68.0) [0.0,0.0,6.0E6]) : kg -> N 68.0 : kg (((gravfield 5.96E24) [0.0, 0.0, 6.0E6]) 68.0) : N ------------------------------ this is why the precendence of f x y is ((f x) y) and of a -> b -> c is (a -> (b -> c)) ------------------ (* COMBINATORS (WITH HISTORICAL NAMES) *) val B = (curry compose); fun B f g x = f(g x); fun W f x = ((f x) x); val twice = (W B); fun by2 x = 2 * x; ((twice by2) 7); ---------------------- ((twice by2) 7) = (((W B) by2) 7) = (((B by2) by2) 7) = (by2 (by2 7)) = 28 -------------------- fun I x = x; fun K c x = c; ((K 3) 5); fun S f g x = ((f x) (g x)); (* type of S is: ('a -> 'b -> 'c) -> ('a -> 'b) -> 'a -> 'c *) (* exercise: what is ((S K) K)? *) ------------------- Anything you can program, you can do with S and K (only)! (if S had the right type, but it doesn't) ------------------- (* FIXPOINT COMBINATOR *) fun Y F x = F (Y F) x; (* type is: (('a -> 'b) -> ('a -> 'b)) -> ('a -> 'b) *) fun Fact f n = if n = 0 then 1 else n * f(n-1); val factorial = Y Fact factorial 3; ------------------- Note that Fact is not recursive! but factorial is the factorial function! factorial 3 = Y Fact 3 = Fact (Y Fact) 3 = if 3 = 0 then 1 else 3 * (Y Fact)(3-1); (* can skip this step *) = (* can skip this step *) 3 * (Y Fact)(2) = 3 * Fact (Y Fact) 2 = 3 * 2 * (Y Fact)(1) = 3 * 2 * Fact (Y Fact) 1 = 3 * 2 * 1 * (Y Fact)(0) = 3 * 2 * 1 * Fact (Y Fact) 0 = 3 * 2 * 1 * 1 = 6 ** functions are the ultimate these ideas will be explored more fully later. *** can be used to implement "infinite" data strucutures because a function describes a potentially infinite mapping streams *** can be used to implement arbitrary control structures. because a function can represent the rest of the program continuations