Closure conversion Higher-order functions Michel Schinz – parts based on slides by X. Leroy Advanced compiler construction, 2008-04-04 Higher-order function HOFs in C In C, it is possible to pass a function as an argument, and to A higher-order function ( HOF ) is a function that either: return a function as a result. • takes another function as argument, or However, C functions cannot be nested: they must all appear • returns a function. at the top level. This severely restricts their usefulness, but Many languages offer higher-order functions, but not all greatly simplifies their implementation – they can be provide the same power... represented as simple code pointers. 3 4 HOFs in functional languages HOF example To illustrate the issues related to the representation of In functional languages – Scala, Scheme, OCaml, etc. – functions in a functional language, we will use the following Scheme example: functions can be nested, and they can survive the scope that defined them. (define make-adder (lambda (x) This is very powerful as it permits the definition of functions (lambda (y) (+ x y)))) that return “new” functions – e.g. functional composition. (define increment (make-adder 1)) However, as we will see, it also complicates the (increment 41) ⇒ 42 representation of functions, as simple code pointers are no longer sufficient. (define decrement (make-adder -1)) (decrement 42) ⇒ 41 5 6
Representing adder functions To represent the functions returned by make-adder , we basically have two choices: Closures 1. Keep the code pointer representation for functions. However, that implies run-time code generation, as each function returned by make-adder is different! 2. Find another representation for functions, which does not depend on run-time code generation. 7 Closures Closure (make-adder 1) (make-adder -1) To adequately represent the functions returned by make- shared adder , their code pointer must be augmented with the value code of x . code code compiled code for: Such a combination of a code pointer and an environment environment environment (lambda (y) giving the values of the free variable(s) – here x – is called a (+ x y)) closure . The name refers to the fact that the pair composed of the x � 1 x � - 1 code pointer and the environment is self-contained. The code of a closure must be evaluated in its environment, so that x is “known”. 9 10 Introducing closures Representing closures During function application, nothing is known about the Using closures instead of function pointers to represent closure being called – it can be any closure in the program. functions changes the way they are manipulated at run time: The code pointer must therefore be at a known and constant • function abstraction builds and returns a closure instead location so that it can be extracted. of a simple code pointer, The values contained in the environment, however, are not • function application extracts the code pointer from the used during application itself: they will only be accessed by closure, and invokes it with the environment as an the function body. This provides some freedom to place additional argument. them. 11 12
Flat closures Recursive closures In flat (or one-block ) closures , the environment is “inlined” Recursive functions need access to their own closure. For into the closure itself, instead of being referred from it. The example: closure plays the role of the environment. (define f (lambda (l) ... (map f l) ...)) (make-adder 1) Several techniques can be used to give a closure access to itself: 1. the closure – here f – can be treated as a free variable, flat closure and put in its own environment – leading to a cyclic code closure, 2. the closure can be rebuilt from scratch, x � 1 3. with flat closures, the environment is the closure, and can be reused directly. 13 14 Mutually-recursive closures Mutually-recursive closures Mutually-recursive functions all need access to the closures cyclic closures shared closure of all the functions in the definition. closure for f closure for g For example, in the following program, f needs access to the closure for f code ptr. f closure of g , and the other way around: closure for g code ptr. g (letrec ((f (lambda (l) …(compose f g)…)) code ptr. f code ptr. g (g (lambda (l) …(compose g f)…))) v 1 …) v 2 Solutions: v 1 w 1 v 3 1. use cyclic closures, or v 2 w 2 w 1 2. share a single closure with interior pointers (note: v 3 interior pointers make the job of the GC harder). w 2 15 16 Closure conversion In a compiler, closures can be implemented by a simplification phase, called closure conversion . Compiling closures Closure conversion transforms a program in which functions can have free variables into an equivalent one containing only closed functions. The output of closure conversion is therefore a program in which functions can be represented as code pointers! 18
Free variables Free variables example Our adder example contains two functions, corresponding to the two occurrences of the lambda keyword: The free variables of a function are the variables that are (define make-adder used but not defined in that function – i.e. they are defined (lambda (x) in some enclosing scope. (lambda (y) (+ x y)))) Global variables are never considered free, since they are The outer one does not have any free variable: it is a closed available everywhere. function , like all top-level functions. The inner one has a single free variable: x . 19 20 Closing functions Closing example (define make-adder Functions are closed by adding a parameter representing the (lambda (x) environment, and using it in the function’s body to access (lambda (y) (+ x y)))) free variables. Function abstraction and application must of course be closure for adapted accordingly: make-adder • abstraction must create and initialise the closure and its (define make-adder environment, (vector (lambda (env 1 x) (vector (lambda (env 2 y) • application must extract the environment and pass it as (+ (vector-ref env 2 1) y)) an additional parameter. closure x)))) for anonymous adder 21 22 Minischeme closure conversion As we have seen, closure conversion consists in closing functions by passing them an environment containig the values of their free variables. Closure conversion for We will specify the closing of minischeme functions as a function C mapping potentially-open terms to closed ones. minischeme For that, we first need to define a function F mapping a term to the set of its free variables. Note: to simplify presentation, we assume in the following slides that all variables in a program have a unique name. 24
Minischeme free variables Closing minischeme functions Closing minischeme constructs that do not deal with F [ (lambda ( v 1 ... ) body 1 ... ) ] = functions or variables is trivial: ( F [body 1 ] � F [body 2 ] � ...) \ { v 1 , ... } C [ (define name value ) ] = F [ (if e 1 e 2 e 3 ) ] = F [e 1 ] � F [e 2 ] � F [e 3 ] (define name C [value] ) F [ ( e 1 e 2 ... ) ] = F [e 1 ] � F [e 2 ] � ... F [v] when v is local = { v } C [ (let (( v 1 e 1 ) ... ) body 1 ... ) ] = F [v] when v is global or a primitive = � (let (( v 1 C [e 1 ] ) ... ) C [body 1 ] ... ) Note: since a let form is equivalent to the application of C [ (if e 1 e 2 e 3 ) ] = an anonymous function, it is easy to deduce the rule to (if C [e 1 ] C [e 2 ] C [e 3 ] ) compute its free variables from the rules above. This is left C [x] where x is a number or identifier = as an exercise. x 25 26 Closing minischeme functions Closing minischeme functions Abstraction is closed by creating and returning the closure, represented as a vector: Finally, application extracts the code pointer from the C [ (lambda ( v 1 … ) body 1 … ) ] = closure, and invokes it with the closure itself as the first (vector (lambda (env v 1 … ) E [ C [body 1 ],F, env ] … ) argument, followed by the other arguments: F 1 F 2 … ) fresh variable C [ ( e 1 e 2 … ) ] when e 1 is not a primitive = where (let ((closure C [e 1 ] )) • E [ t , f , e ] transforms t by replacing all occurrences of the ((vector-ref closure 0) closure C [e 2 ] … )) variables of f by accesses to corresponding slots in the C [ ( e 1 e 2 … ) ] when e 1 is a primitive = environment e . ( e 1 C [e 2 ] … ) • F = F [ (lambda ( v 1 … ) body 1 … ) ] and F i is its i th component. 27 28 Closures and objects There is a strong similarity between closures and objects: closures can be seen as objects with a single method – containing the code of the closure – and a set of fields – the environment. Closures and objects In Java, the ability to define nested classes can be used to simulate closures, but the syntax is too heavyweight to be used often. In Scala, a special syntax exists for anonymous functions, which are translated to nested classes. 30
Recommend
More recommend