closure conversion higher order functions
play

Closure conversion Higher-order functions Michel Schinz parts - PDF document

Closure conversion Higher-order functions Michel Schinz parts based on slides by Xavier Leroy 20070427 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


  1. Closure conversion Higher-order functions Michel Schinz – parts based on slides by Xavier Leroy 2007–04–27 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 • returns a function. appear at the top level. This severely restricts their Many languages offer higher-order functions, but not all usefulness, but greatly simplifies their implementation – provide the same power... they can be 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 functions can be nested, and they can survive the scope following Scheme example: that defined them. (define make-adder This is very powerful as it permits the definition of functions (lambda (x) (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

  2. 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 code value of x . code code compiled code for: Such a combination of a code pointer and an environment environment environment (lambda (x) giving the values of the free variable(s) – here x – is called a (lambda (y) closure . (+ x y))) The name refers to the fact that the pair (code pointer, x � 1 x � - 1 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 Using closures instead of function pointers to represent During function application, nothing is known about the functions changes the way they are manipulated at run closure being called – it can be any closure in the program. time: The code pointer must therefore be at a known and • function abstraction builds and returns a closure instead constant location so that it can be extracted. of a simple code pointer, The content of the environment, however, is not used • function application extracts the code pointer from the during application itself: it will only be accessed by the closure, and invokes it with the environment as an function body. This provides some liberty to represent it. additional argument. 11 12

  3. 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: • 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, • the closure can be rebuilt from scratch, x � 1 • with flat closures, the environment is the closure, and can be reused directly. 13 14 Mutually-recursive closures Mutually-recursive closures cyclic closures shared closure Mutually-recursive functions all need access to the closures of all the functions in the definition. closure for f closure for g closure for f For example, in the following program, f needs access to code ptr. f the closure of g , and the other way around: closure for g code ptr. g code ptr. f code ptr. g (letrec ((f (lambda (l) …(compose f g)…)) v 1 (g (lambda (l) …(compose g f)…))) …) v 2 v 1 w 1 Solutions: v 3 v 2 w 2 1. use cyclic closures, or w 1 2. share a single closure with interior pointers. v 3 w 2 15 16 Closure conversion In a compiler, closures can be implemented by a simplification phase, called closure conversion . Closure conversion transforms a program in which Compiling closures functions can be nested and have free variables into an equivalent one containing only top-level – and hence closed – functions. The output of closure conversion is therefore a program in which functions can be represented as code pointers! 18

  4. Closure conversion phases Free variables Closure conversion can be split in two phases: The free variables of a function are the variables that are • the closing of functions, through the introduction of used but not defined in that function – i.e. they are defined environments, in some enclosing scope. • the hoisting of nested, closed functions to the top level. Global variables are never considered free, since they are We will examine them later, but we first need to define the available everywhere. concept of free variable. 19 20 Free variables example Closing functions Functions are closed by adding a parameter representing Our adder example contains two functions, corresponding the environment, and using it in the function’s body to to the two occurrences of the lambda keyword: access free variables. (define make-adder Function abstraction and application must of course be (lambda (x) adapted accordingly: (lambda (y) (+ x y)))) • abstraction must create and initialise the closure and its The outer one does not have any free variable: it is a closed environment, function , like all top-level functions. The inner one has a • application must extract the environment and pass it as single free variable: x . an additional parameter. 21 22 Closing example Hoisting functions (define make-adder (lambda (x) Once they are closed, nested anonymous functions can (lambda (y) (+ x y)))) easily be hoisted to the top level and given an arbitrary name. The original occurrence of the nested function is simply closure for replaced by that name. make-adder (define make-adder After hoisting, all functions appearing in the program are at (vector (lambda (env 1 x) the top-level, and are of course closed. Therefore, they can (vector (lambda (env 2 y) be represented by simple code pointers, as in C. (+ (vector-ref env 2 1) y)) closure x)))) for anonymous adder 23 24

  5. Hoisting example (define make-adder (vector (lambda (env 1 x) (vector (lambda (env 2 y) (+ (vector-ref env 2 1) y)) x)))) Closure conversion for minischeme (define lambda 3 (lambda (env 2 y) (+ (vector-ref env 2 1) y))) (define lambda 4 (lambda (env 1 x) (vector lambda 3 x))) (define make-adder (vector lambda 4 )) 25 Minischeme closure conversion Minischeme free variables As we have seen, closure conversion can be performed by F [ (define name value ) ] = ∅ first closing functions, and then hoisting nested functions to F [ (lambda ( v 1 ... ) body 1 ... ) ] = the top level. ( F [body 1 ] ∪ F [body 2 ] ∪ ...) \ { v 1 , ... } We will look in detail at the closing part for minischeme, F [ (let (( v 1 e 1 ) ... ) body 1 ... ) ] = which we will specify as a function C mapping potentially- ( F [e 1 ] ∪ ... ∪ F [body 1 ] ∪ ...) \ { v 1 , ... } open terms to closed ones. F [ (if e 1 e 2 e 3 ) ] = F [e 1 ] ∪ F [e 2 ] ∪ F [e 3 ] For that, we first need to define a function F mapping a term to the set of its free variables. F [ ( e 1 e 2 ... ) ] = F [e 1 ] ∪ F [e 2 ] ∪ ... Note: to simplify presentation, we assume in the following F [v] when v is local = { v } slides that all variables in a program have a unique name. F [v] when v is global or a primitive = ∅ 27 28 Closing minischeme functions Closing minischeme functions Closing minischeme constructs that do not deal with Abstraction is closed by creating and returning the closure, functions or variables is trivial: represented as a vector: C [ (define name value ) ] = C [ (lambda ( v 1 … ) body 1 … ) ] = (define name C [value] ) (vector (lambda (env v 1 … ) E [ C [body 1 ],F, env ] … ) F@1 F@2 … ) C [ (let (( v 1 e 1 ) ... ) body 1 ... ) ] = (let (( v 1 C [e 1 ] ) ... ) C [body 1 ] ... ) where • E [ t , f , e ] transforms t by replacing all occurrences of the C [ (if e 1 e 2 e 3 ) ] = variables of f by accesses to corresponding slots in the (if C [e 1 ] C [e 2 ] C [e 3 ] ) environment e . C [n] where n is a number = • F = F [ (lambda ( v 1 … ) body 1 … ) ] n 29 30

Recommend


More recommend