CSC447

Concepts of Programming Languages

Closure Implementation

Instructor: James Riely

Closures

  • Runtime support for nested functions
    • particularly when lifetimes do not nest
  • Only applies to static / lexical scope

Top-Level Functions

  • Function declarations made at top level
  • Not hidden by scope

int loop (int n, int result) {
  if (n <= 1) {
    return result;
  } else {
    return loop (n - 1, n * result);
  }
}
int fact (int n) {
  return loop (n, 1);
}
          

Nested Functions - GCC

  • Nested functions allow for reuse of inner function name
  • Allowed by GCC, but not C standard

int fact (int n) {
  int loop (int n, int result) {
    if (n <= 1) {
      return result;
    } else {
      return loop (n - 1, n * result);
    }
  }
  return loop (n, 1);
}
          

$ gcc -c nested-fact.c 
$ gcc -pedantic -c nested-fact.c 
function.c: In function ‘fact’:
function.c:2:3: warning: ISO C forbids nested functions [-pedantic]
          

Nested Functions - GCC

  • Access variables from enclosing context
    • requires some runtime support

int fact (int n) {
  int loop (int i, int result) {
      if (i > n) {
      return result;
    } else {
      return loop (i+1, i * result);
    }
  }
  return loop (1, 1);
}
          

Why Nested Functions? Scala Example


def printElt [A,B] (f:A=>B) (x:A) : B = 
  println (x)
  f (x)

def mapDebug [A,B] (xs:List[A], f:A=>B) : List[B] = 
  xs.map (printElt (f))
          

def mapDebug [A,B] (xs:List[A], f:A=>B) : List[B] = 
  def printElt (x:A) : B = 
    println (x)
    f (x)  // use f from enclosing context
  xs.map (printElt)
          

def mapDebug [A,B] (xs:List[A], f:A=>B) : List[B] = 
  xs.map ((x:A) => { println (x); f (x) })  // anonymous function clearer
          

Nested Functions - Scope vs Lifetime

  • Limit scope of inner function
  • Lifetime of inner function vs lifetime of outer function?
  • Potentially unsafe, and requires more runtime support
    • more support than accessing variables from enclosing function

Nested Functions - GCC

If you try to call the nested function through its address after the containing function exits, all hell breaks loose. If you try to call it after a containing scope level exits, and if it refers to some of the variables that are no longer in scope, you may be lucky, but it's not wise to take the risk. If, however, the nested function does not refer to anything that has gone out of scope, you should be safe.

Nested Functions - GCC

  • Lifetime problems caused by nested functions

#include <stdio.h>
#include <stdlib.h>
typedef void (*funcptr) (int);
funcptr f (int x) {
  void g (int y) {
    printf ("x = %d, y = %d\n", x, y);
  }
  g (1);
  return &g;
}
int main (void) {
  funcptr h = f (10);
  (*h) (2);
  f (20);
  (*h) (3);
}

Nested Functions - GCC

  • Unsafe calls may or may not work

$ gcc -std=c99 nested-gcc.c
$ ./a.out
x = 10, y = 1 <- safe to call g, with x=10
x = 10, y = 2 <- unsafe to call h, created with x=10, GOOD!
x = 20, y = 1 <- safe to call g
x = 20, y = 3 <- unsafe to call h, created with x=10, BAD!
          

Nested Function - Clang

Nested Function - Clang


#include <stdio.h>
#include <stdlib.h>
#include <Block.h>
// ^funcptr for blocks; *funcptr for function pointers
typedef void (^funcptr) (int);
funcptr f (int x) {
  funcptr g;
  g = ^(int y) {
    printf ("x = %d, y = %d\n", x, y); // use x from enclosing defn
  };
  g = Block_copy (g);
  g (1); // OK, f's activation record still allocated
  return g;
}
int main (void) {
  funcptr h = f (10);
  h (2); // OK, because of Block_copy
  f (20);
  h (3); // OK, because of Block_copy
  Block_release	(h);
}
          

Nested Function - Clang

  • Blocks support is not turned on by default on Ubuntu

$ sudo apt-get install libblocksruntime-dev
          
  • Correct output

$ clang -fblocks nested-clang.c -lBlocksRuntime
$ ./a.out
x = 10, y = 1
x = 10, y = 2
x = 20, y = 1
x = 10, y = 3 <- safe to call h, created with x=10, GOOD!
          

Nested Function - Clang

  • Without Block_copy and Block_release

$ clang -fblocks nested-clang.c -lBlocksRuntime
$ ./a.out 
x = 10, y = 1
x = -1035955720, y = 2  <- unsafe to call h, created with x=10, BAD!
x = 20, y = 1
x = -1035955720, y = 3  <- unsafe to call h, created with x=10, BAD!
          

Nested Function - Swift

  • Nested functions work correctly in Swift

func f (_ x:Int) -> (Int) -> () {
  func g (_ y:Int) -> () { print ("x = " + String(x) + " y = " + String(y)) }
  g (1)
  return g
}
func main () {
  let h = f (10)
  h (2)
  let _ = f (20)
  h (3)
}
main()
          

$ swiftc nested-swift.swift 
$ ./nested-swift
x = 10 y = 1
x = 10 y = 2
x = 20 y = 1
x = 10 y = 3
          

Nested Function - Scala

  • Nested functions work correctly in Scala

def f (x:Int) : Int=>Unit =
  def g (y:Int) : Unit = println ("x = %d, y = %d".format (x, y))
  g (1)
  g

def main () = 
  val h = f (10)
  h (2)
  f (20)
  h (3)

main()
          

x = 10, y = 1
x = 10, y = 2
x = 20, y = 1
x = 10, y = 3 <- safe to call h, created with x=10, GOOD!
          

Nested Function - Java 8

  • Nested functions work correctly in Java 8

import java.util.function.IntConsumer;
public class NestedFunc1 {
  static IntConsumer f (int x) {
    IntConsumer g = y -> System.out.format ("x = %d, y = %d%n", x, y);
    g.accept (1);
    return g;
  }
  public static void main (String[] args) {
    IntConsumer h = f (10);
    h.accept (2);
    f (20); 
    h.accept (3);
  }
}
          

Nested Function - Java 8


$ javac NestedFunc1.java 
$ java NestedFunc1
x = 10, y = 1
x = 10, y = 2
x = 20, y = 1
x = 10, y = 3 <- safe to call h, created with x=10, GOOD!
          

Closures and Mutability

  • Recall javac requires final i from enclosing scope
    
    for (int i = 0; i < 5; i++) { 
      new Thread (new Runnable () {
          public void run () { /* rejected: i mutates */
            while (true) { System.out.print (i); }
          }
        }).start ();
    }
                  
  • So a copy is made
    
    for (int i = 0; i < 5; i++) { 
      int x = i; 
      new Thread (new Runnable () {
          public void run () { /* accepted: x never mutates */
            while (true) { System.out.print (x); }
          }
        }).start ();
    }
                  

Closures and Mutability

  • The same holds for lambda expressions
    
    for (int i = 0; i < 5; i++) { 
      new Thread (() -> { /* rejected: i mutates */
            while (true) { System.out.print (i); }
          }).start ();
    }
                  
  • So a copy is made
    
    for (int i = 0; i < 5; i++) { 
      int x = i; 
      new Thread (() => { /* accepted: x never mutates */
            while (true) { System.out.print (x); }
        }).start ();
    }
                  

Closures and Mutability

  • Scala allows closures over mutable variables!
    
    var i = 1
    while i < 5 do
      Thread (() => 
        while true do print (i)
      ).start
      i = i + 1
                  
  • We really need a copy...
    
    var i = 1
    while i < 5 do
      val x = i
      Thread (() => 
        while true do print (x)
      ).start
      i = i + 1
                  

Closures and Mutability

  • Immutable programming is better!
    
    def main(args: Array[String]): Unit =
      for i <- (1 to 4) do
        Thread (() => 
          while true do print (i)
        ).start
                  

Why is this tricky?

  1. enclosing function outer is called
    • AR contains data x
  2. outer returns nested function inner
    • inner references x from outer's AR
    • lifetime of outer's AR and x ends
  3. nested function inner is called
    • needs x from outer's AR

def outer (x:A) : B=>C = 
  def inner (y:B) : C = 
    //...use x and y...
  inner
          

Implementation: Closures

  • Closures store inner function and environment
  • Environment contains variables from enclosing scope
  • Lifetime of environment = lifetime of inner function
    • environment is allocated on the heap
  • Different implementations in different PLs
  • Recurring implementation choice: copy or share?

Implementation Choice

  • Closure contains
    • pointer/reference to code for inner
    • a copy of x

def outer (x:A) : B=>C = 
  def inner (y:B) : C = 
    ...use x and y...
  inner
          

Implementation Choice

  • Closure contains
    • pointer/reference to code for inner
    • copies of x and u
  • inner sees updated u?
  • Require u to be immutable?

def outer (x:A) : B=>C = 
  var u:A = x
  def inner (y:B) : C = 
    //...use u and y...
  u = u + 1
  inner
          

Implementation Choice

  • Alternatively, share u
  • Closure contains
    • pointer/reference to code for inner
    • copy of x
    • reference to shared u (on heap)

def outer (x:A) : B=>C = 
  var u:A = x
  def inner (y:B) : C = 
    //...use u and y...
  u = u + 1
  inner
          

Scala2 Implementation


object Closure:
  def outer (x:Int) : Boolean=>Int = 
    def inner (y:Boolean) : Int = 
      x + (if y then 0 else 1)
    inner
          

$ scalac Closure.scala

$ ls -1 Closure*
Closure$$anonfun$outer$1.class
Closure.class
Closure$.class
Closure.scala
          

Scala2 Implementation

  • The closure is an instance of the second class
  • x copied into field x$1

$ javap -p Closure
Compiled from "Closure.scala"
public final class Closure {
  public static scala.Function1<java.lang.Object, java.lang.Object> outer(int);
}

$ javap -p Closure\$\$anonfun\$outer\$1
Compiled from "Closure.scala"
public final class Closure$$anonfun$outer$1 extends scala.runtime.AbstractFunction1<java.lang.Object, java.lang.Object> {
  private final int x$1;
  public final int apply(boolean);
  public Closure$$anonfun$outer$1(int);
}
          
(some parts removed)

Scala2 Implementation

  • u is a var declaration, so is mutable

object Closure:
  def outer (x:Int) : Boolean=>Int =
    var u:Int = x
    def inner (y:Boolean) : Int =
      x + u + (if y then 0 else 1)
    inner
          

Scala2 Implementation

  • x copied into field x$1
  • u shared on heap via reference in field u$1

$ javap -p Closure\$\$anonfun\$outer\$1
Compiled from "Closure.scala"
public final class Closure$$anonfun$outer$1 extends scala.runtime.AbstractFunction1<java.lang.Object, java.lang.Object> {
  private final int x$1;
  private final scala.runtime.IntRef u$1;
  public final int apply(boolean);
  public Closure$$anonfun$outer$1(int, scala.runtime.IntRef);
}
          
(some parts removed)

Scala2 Implementation

  • The closure is an instance of the second class
  • x copied into field x$1

$ javap -p Closure
Compiled from "Closure.scala"
public final class Closure {
  public static scala.Function1<java.lang.Object, java.lang.Object> outer(int);
}

$ javap -p Closure\$\$anonfun\$outer\$1
Compiled from "Closure.scala"
public final class Closure$$anonfun$outer$1 extends scala.runtime.AbstractFunction1<java.lang.Object, java.lang.Object> {
  private final int x$1;
  public final int apply(boolean);
  public Closure$$anonfun$outer$1(int);
}
          
(some parts removed)

Further reading

  • There are some great resources about closures in the javascript world, such as this