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:
List.each
: iterate a listList.eachr
: reverse iterate a listMap.each
: iterate a map
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 ofits
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 }