18. Closures

Overview

Closures are an expression to create a function inside the body of a method. Closures have the ability to reference local variables from their enclosing scope. This ability to create inline functions which access local scope makes it easy to use closures as method arguments. For instance closures are used extensively as an iteration mechanism.

Syntax

The basic syntax of a closure:

|A a, B b...->R| { stmts }

The start of a closure is its signature which reuses the same syntax as function types. The body of the closure is a series of zero or more statements. The return statement is used to return a result and exit out of the closure (Fantom doesn't support any other way to jump out of a closure other than return or throw). Let's look a simple example:

f := |->| { echo("hi there") }
f()
f()

The code above creates a closure that prints a message to the console. If we run the code above "hi there" is printed twice. We are assigning the closure to the variable f. The closure itself is an expression which creates an instance of Func - just like 8 is an expression which creates an Int. The signature of the function is |->| which means that the function takes no arguments and returns Void. Once the closure is assigned to f, we can call f like any other function.

Here is another example:

f := |Int a, Int b->Int| { return a + b }
nine := f(4, 5)

The code above declares a closure which accepts two Ints and returns their sum. Notice the closure uses the return statement to return the result (later we'll see how we can omit it).

Binding Locals

The real power of a closure is its ability to bind to the local variables in its enclosing scope. Consider this example:

counter := 0
f := |->Int| { return ++counter }
echo(f())
echo(f())
echo(f())
echo(counter)

This example creates a function which returns an Int and then calls the function three times. Note how the body of the closure uses the local variable counter. The closure has access to both read and write any variable in its enclosing scope - just like an if statement or a while loop. So the output of the code above is to print "1", "2", "3", and "3".

Scope Lifetime

When a closure binds to a local variable in its outer scope, that variable lives as long as the closure lives. Remember that closures are just Func objects which can be passed outside of the original scope. Consider this example:

static Func createFunc()
{
  counter := 0
  return |->Int| { return ++counter }
}

static Void main()
{
  f := createFunc
  echo(f())
  echo(f())
  echo(f())
}

The createFunc method returns a closure function bound to the local variable counter. The local variable will exist as long as the closure exists. In this case the main method assigns the function to the variable f then calls it three times. The output will print "1", "2", and "3". Effectively this allows closures to store their own state between invocations.

Binding This

If a closure is declared inside an instance method, then a closure can bind this variable just like any other local:

Str first := "Bart"
Str last  := "Simpson"

Void test()
{
  f := |->Str| { return first + " " + this.last }
  echo(f())
}

The code above illustrates binding to two local slots. The closure binds to first with an implicit this. The closure uses an explicit this to bind to last. Note that the this keyword references the enclosing method's instance, not the the closure object. This also means generic Obj methods like toStr and type reference the enclosing method instance, not the closure instance.

Multiple Closures

When a method declares multiple closures, the closures all share the same local variables:

counter := 0
f := |->Int| { return ++counter }
g := |->Int| { return ++counter }
echo(f())
echo(g())
echo(f())
echo(g())

The code above prints "1", "2", "3", "4" because both f and g share the same binding to counter.

Note: in the current implementation all closures share the same set of locals. This means that any closure holding a reference to those locals will prevent garbage collection of all closure variables.

Closure Parameters

A closure is just a normal expression and can be passed as an argument to a method call which expects a Func parameter. Many key APIs are designed to work with functions. For example consider the List.findAll method which returns a sub-list of every item matching a criteria. Since we want to leave the match criteria open ended, findAll lets you pass in an arbitrary function to determine matches.

Let's consider an example for finding all the even numbers in a list:

list := [0, 1, 2, 3, 4]
f := |Int v->Bool| { return v%2==0 }
evens := list.findAll(f)

The code above creates a function, then passes it to the findAll method. Since the closure is just an expression we could also rewrite the code as:

evens := list.findAll(|Int v->Bool| { return v%2==0 })

Since closures are used heavily in this way, Fantom supports a special syntax borrowed from Ruby. If a closure is the last argument to a method call, then the closure can be pulled out as a suffix to the call:

evens := list.findAll() |Int v->Bool| { return v%2==0 }

Since we aren't passing any arguments other than the closure we can simplify this code even further by removing the parens:

evens := list.findAll |Int v->Bool| { return v%2==0 }

Iteration

Closures are designed to be the primary mechanism of iteration. Key methods which accept a function parameter:

When iterating a list both the value and the integer index are passed to the closure:

list := ["one", "two", "three"]
list.each |Str val, Int index| { echo("$index = $val") }

But remember that we don't have to use all the arguments provided to the function. For example if we don't care about the integer index:

list := ["one", "two", "three"]
list.each |Str val| { echo(val) }

Map iteration works the same way:

map := [1:"one", 3:"three", 5:"five"]
map.each |Str val, Int key| { echo("$key=$val") }
map.each |Str val| { echo(val) }

Closure Type Inference

Closures which are passed as the last argument to a method support type inference:

// fully specified closure signatures
list := ["one", "two", "three"]
list.each |Str v, Int i| { echo("$i = $v") }
list.each |Str v| { echo(v) }

// inferred closure signatures
list.each |v, i| { echo("$i = $v") }
list.each |v| { echo(v) }

If you leave the types off the closures parameters, then they are inferred based on the closure's context. In the example above a closure passed to Str[].list.each is inferred to have a type of |Str,Int|.

You can also use inference in conjunction with a return type or you can omit the return type entirely:

odds = [1, 2, 3, 4, 5].findAll |v->Bool| { v.isOdd }
odds = [1, 2, 3, 4, 5].findAll |v| { v.isOdd }

Closures can only infer the type when they are being passed to a method which expects a function. If a closure's parameters cannot be inferred then the defaults to Obj?:

// closure with inferred type of |Obj? v|
f := |v| { echo(v) }

It-Blocks

It-blocks are a special form of closures with the following differences:

  • They omit a function signature and are declared only with curly braces
  • Use type inference based on their context
  • Define an implicit single parameter called it
  • Define an implicit scope for it
  • Return keyword is not allowed in an it-block
  • It-blocks are given compile time permission to set const fields on the it parameter, although runtime checks will throw ConstErr if an attempt is made to set a const field outside of its constructor (see const fields)

An it-block can be used whenever a single parameter function is expected:

["a", "b", "c"].each |Str s| { echo(s.upper) }  // long hand
["a", "b", "c"].each { echo(it.upper) }         // short hand

In the example above, the it-block is a closure with an implicit Str parameter called it.

The it parameter works just like the implicit this parameter in an instance method. If a given identifier is not declared in the local scope, then we attempt to bind to it:

["a", "b", "c"].each { echo(it.upper) }  // explicit it call
["a", "b", "c"].each { echo(upper) }     // implicit it call

Just like this, if a local variable shadows a slot on it, then the local variable is used. If an attempt is made to implicitly access a slot which exists on both this and it, then it is a compile time error:

["a", "b", "c"].each { echo(toStr) }        // Ambiguous slot error
["a", "b", "c"].each { echo(it.toStr) }     // explicit call on it
["a", "b", "c"].each { echo(this.toStr) }   // explicit call on this

This Functions

As a general rule the sys::This type is reserved for use only as the return type of instance methods. There is one exception - you are allowed to declare a method parameter typed as |This| to indicate that an it-block function is expected:

new make(|This| f) { f(this) }

With-Blocks

Fantom allows you to append an it-block to any expression. Whenever an it-block is used and a function is not expected, then the compiler generates a call to Obj.with:

list := Str[,].with { fill("x", 3) }   // explicit call to with
list := Str[,] { fill("x", 3) }        // implicit call to with

The default implementation of Obj.with just applies the function:

virtual This with(|This| f)
{
  f(this)
  return this
}

Using it-blocks and Obj.with allows you open a new lexical scope with any expression. It is quite useful for declarative programming.

With blocks are commonly used with the comma add operator to implicitly add items to a collection.

// this long-hand syntax
pane.with
{
  add(child1)
  add(child2)
  add(child3)
}

// can be collapsed to this with-block and comma operator
pane
{
  child1,
  child2,
  child3
}