CS558 Programming Languages Fall 2016 Lecture 6b

Andrew Tolmach Portland State University

© 1994-2016 Semantics of first-class functions What’s in the “value” of a first-class function f ? Roughly speaking, just f’s definition (its parameters and body expression) But nested functions can have free variables defined in an enclosing , and the behavior of the function depends on their values. To find those values, it suffices to record the environment surrounding the declaration of f Store this in a “closure” representing f But: must make sure locations used in environment stay live as long as closure does! 2 Semantics of first-class functions case class ClosureV(x:String,b:Expr,e:Env) extends Value def interpE(env:Env,expr:Expr) : Value = expr match { case Fun(x,e) => ClosureV(x,e,env) case App(f,e) => interpE(env,f) match { case ClosureV(x,b,cenv) => val a = stack.push(1) val v = interpE(env,e) set(a,v) val r = interpE(cenv + (x -> a),b) stack.pop(1) r case _ => throw InterpException (…) } case Let(x,e,b) => … But using stack allocation for locals and parameters … won't work! }

3 Trouble with first-class functions Problem: if a closure value is returned, the environment may point to locations that have been popped from the stack!

(let f (fun (x)(fun (y)(+ x y))) (let g (@ f 42) …)) At this point, stack storage for x has been popped. So we’re in trouble if we call g.

Similar problems can occur if closure is stored into a heap data structure.

You will explore these scenarios in this week’s lab. First-class functions need the heap

Bottom line: if we want the flexibility of fully first-class functions, we cannot store their free variables on the stack.

They must instead live in the heap. How to arrange this?

Simple solution: just allocate all activation record data in the heap instead of the stack, and rely on garbage collection to deallocate and re-use them. (A few compilers do this. See this week’s lab!)

More refined solution: Heap-allocate just those variables that are free in some closure expression. This is usually done by adding an extra layer of boxing around such variables. (Some compilers do this.)

Pure functional language solution: if the free “variables" are actually immutable, we can store copies of their values in a heap closure record. (Most functional language compilers do this.) The tyranny of the stack

Many older languages support functions as values, but not in fully first- class way—usually to avoid the need to store variables in the heap.

For example, some languages with nested functions (e.g. Pascal, Modula, Ada) allow functions to be passed downwards as parameters to other functions, but not returned or stored.

Under this restriction, if function f defines nested function g, it is impossible for g to be called after f has returned. (Why?)

So the compiler can use conventional stack storage, because every free variable of a function lives in an activation record that is still on the stack when the function might be called. (But how to find it?)

On the other hand, C allows fully first-class functions, but has no nested functions. Hence functions have no free variables and closures become simple code pointers. Closures vs. Objects First-class function closures provide a way to package a function with some (relatively) fixed parameters (its free variables)

Objects can be used to give a similar effect: case class Filter(p: Int => Boolean) { def apply(xs:List[Int]) : List[Int] = xs match { case Nil => Nil case (y::ys) => if (p(y)) y::apply(ys) else apply(ys) } } A bit more verbose Use of .apply than closures becomes pervasive val evens = Filter(x => x%2 == 0) val v = evens.apply(List(1,2,3,4)) // yields List(2,4) Call-by-name using closures Recall semantics of call-by-name: bind formal parameter to actual argument expression, with free variables resolved in caller foo x y = if x = 0 then x else y let x = 1000000 in foo 1 (factorial x) We can implement this using “thunks”: first-class functions of zero arguments foo x y = if x() = 0 then x() else y() let x = 1000000 in foo (fun () => 1) (fun () => factorial x)

Shows that CBN introduces significant overhead Pure

Idea: compose programs out of pure functions that have no side-effects

Software engineering advantage: greatly improves modularity, because no hidden interactions between functions

Pure functions won't interfere if evaluated in parallel

Works well with call-by-name/need (in impure languages we need to worry about evaluation order)

I/O is problematic, but Haskell has a good solution: program computes an I/O action which is then performed

ML family is impure: no variables, but mutable heap structures (e.g. boxes) and explicit I/O Explicit recursion First-class functions are very powerful! With them, we can encode many useful language features without extending the language itself

Example: recursion via fixed points

Suppose we have only anonymous functions and (non-recursive) let bindings. How can we write a recursive function?

(let fact (fun (n) (if (<= n 0) Oops! Not in scope! 1 (* n (fact (- n 1))))) …)

Abstracting over recursive function

Problem: we want to define fact such that fact == �n.…fact…

Trick: rewrite RHS by abstracting over fact itself fact == (�f.�n.…f…)fact function application Or, equivalently: fact == h fact where h = �f.�n.…f…

Note that h is a perfectly ordinary function that does not use recursion Fixed points

The factorial function is a solution to the equation fact == h fact i.e. a function fact such that the result of applying h to fact is just fact again. We say that fact is a fixed point of the function h.

It is easy to find the fixed points of some functions. E.g. the function �x.x * x has both 0 and 1 as fixed points. In general, a function may have 0, 1, some, or infinitely many fixed points. Fixed point of factorial

It is not so easy to see what the fixed point of h should look like, but let’s just assume the existence of a function Y that takes any function as argument and returns a fixed point for that function as result, so f (Y f) == Y f for any f

In particular, h (Y h) == Y h, so fact = Y h is a definition of factorial that does not involve recursion! Example With our definitions, we can now compute, e.g., fact(1):

h = �f.�n.if n <= 0 then 1 else n*(f(n-1)) fact = Y h Using substitution fact 1 = semantics (Y h) 1 == (h (Y h)) 1 = (�f.�n.if n <= 0 then 1 else n*(f(n-1)) (Y h)) 1 = (�n.if n <= 0 then 1 else n*((Y h)(n-1))) 1 = if 1 <= 0 then 1 else 1*((Y h)(1-1)) = 1 * ((Y h) 0) = 1 * (h (Y h)) 0) = 1 * (�f.�n.if n <= 0 then 1 else n*(f(n-1)) (Y h)) 0 = 1 * (�n.if n <= 0 then 1 else n*((Y h)(n-1))) 0 = 1 * (if 0 <= 0 then 1 else 0 * ((Y h)(0-1))) = 1 * 1 = 1 Defining Y

So, recursion can be expressed wholly in terms of the fixed-point combinator Y.

But how do we define Y?

We could build it into our language (see this week’s lab).

But amazingly, it is also possible to define Y as just another ordinary function! Here is one definition:

Y =�f.(�x.f (�y.((x x) y)))(�x.f (�y.((x x) y)))