Closures

A brace in expression position is always a closure with inferred or written parameters, var ones too; it captures a const binding as a copy and a var binding as itself, which only a closure handed to a parameter that just calls it may do.

Diese Seite gibt es noch nicht auf Deutsch, deshalb steht sie hier auf Englisch. Zur englischen Seite

There is one closure form. A { in expression position is always a closure, never a block, and it captures the names of the scope it was written in.

Example#

const double = { x: Int => x * 2 }
const triple: (Int) => Int = { _ * 3 }
const add: (Int, Int) => Int = { a, b => a + b }

print double(4)
print triple(4)
print add(2, 3)

Syntax#

{ [var] <parameter>[: <Type>], ... => <statement>* <result expression>? }
{ <statement>* <result expression>? }              // Implicit parameters _, _2, _3, ...

Rules#

  1. A { in expression position is always a closure. There is no block expression, so { 1 } is a closure of no parameters that answers 1, never a scope that runs and discards its own bindings.

  2. A parameter's type is inferred from the expected type, and has to be written out where there is none. const double = { x: Int => x * 2 } needs the annotation; const double: (Int) => Int = { x => x * 2 } does not, because the binding's type says what x is.

    Kompiliert nicht
    const double = { x => x * 2 }
    // error: The parameters of this closure need types: `{ value: Int => ... }`
  3. A closure with no written parameter list uses the implicit parameters _, _2, _3, one per position. { _ * 3 } is a closure of one parameter, { _ + _2 } of two. Naming even one parameter switches the whole closure to named parameters; _ and a name cannot mix in the same closure.

    A tuple position of such a parameter is still written behind a dot: { _.0 }, never { 0 }, because a bare 0 is the number and nothing else.

    const pairs = [(1, "one"), (2, "two")]
    print pairs.map({ _.0 }).toList()

    An implicit parameter is what a named one would be: where the expected function type takes the parameter as var, _ is a var and changes the caller's value, exactly as { items => items.append 2 } does.

    fn filled(body: (var items: List<Int>) => Void): List<Int> {
      var items: List<Int> = [1]
      body items
      items
    }
    
    print filled({ _.append 2 })
    // prints [1, 2]
  4. The result type is always inferred, never annotated. A closure that needs a written result type is a local fn instead, because fn is a declaration and can carry a return type (see Declaring a function); a fn declared by name can be passed exactly where a closure is expected.

  5. return and ? inside a closure return from the closure, not from the function around it. There is no non-local return: the value return produces is the closure's own result, at the point the closure is called, and a ? hands its failure to whoever called the closure - so a closure with a ? in it has to produce an Option or a Result (see The question mark operator, rule 8).

    const clamp = { x: Int, low: Int, high: Int =>
      if x < low { return low }
      if x > high { return high }
      x
    }
    
    print clamp(15, 0, 10)
  6. A const binding is captured as a copy. Reading it inside the closure never sees a later assignment to the outer name, because the outer name cannot be reassigned either (see Bindings).

  7. A var binding is captured as itself, shared between the closure and the scope it was written in. Both sides see every change; this is the one place in the language where a variable is shared rather than copied.

    fn total(numbers: List<Int>): Int {
      var sum = 0
      numbers.forEach { sum = sum + _ }
      sum
    }
    
    print total([1, 2, 3])
  8. A closure that captures a var binding may only run while the binding exists, and the checker enforces it. A local var, a var parameter, a var fn receiver and a var parameter of a closure around it are all the same: such a closure may stand straight as the argument of a call whose parameter only calls it, and nowhere else. Bound to a name, stored in a field, a collection or a case, returned, handed to spawn or to a parameter that keeps it, it is an error. A parameter only calls its closure when it has a function type, its function has a body, and that body calls it (action(value), action value), calls it inside a closure that itself only runs during the call, or hands it on by name to a parameter that only calls it - which is what forEach, unless, a receiver closure and every block of a DSL do. A lazy stage keeps its closure: items.map { ... } stores it in the stage it answers, so a closure given to map or filter may read a var only through a const copy of it.

    Kompiliert nicht
    fn counter(): () => Int {
      var count = 0
      {
        count = count + 1
        count
      }
    }
    // error: This closure captures the `var` binding `count` and may outlive it
    Kompiliert nicht
    fn makeIncrementer(var target: Int): () => Void {
      {
        target = target + 1
      }
    }
    // error: This closure captures the `var` parameter `target` and may outlive the call

    The fix is to hand the value to a var parameter of the function that runs the closure, or to let the closure return what it computed. The rule is what keeps two copies of a value from ever sharing a variable through a closure they hold, a closure from changing a binding while a var access to it runs, and a task from reaching one.

  9. var in front of a parameter binds the caller's place, and the function type has to take it as var too. { var point => ... } fills a (var value: Point) => Void: a write to point lands in the value the caller handed over, as it does through a var parameter of a fn, and the closure may read the bindings around it while it does, which a named fn cannot. Where nothing is expected, { var point: Point => ... } is a closure of the type (var point: Point) => Void. Where a function type is expected, a parameter it takes as var is a var in the closure with or without the word (rule 3); the word says so where the reader sees it. A var parameter is a reference, so a closure written inside that captures it is held to rule 8.

    type Point {
      var x: Float
    }
    
    fn forEachPoint(var points: List<Point>, body: (var value: Point) => Void) {
      for index in 0..points.length() {
        body points[index]
      }
    }
    
    var points = [Point(1.0), Point(2.0)]
    const step = 0.5
    forEachPoint points { var point =>
      point.x = point.x + step
    }
    print points
    // prints [Point(x: 1.5), Point(x: 2.5)]

    A var the function type does not have is an error of the closure. A function value whose parameters differ from the expected ones only in a var is not of the expected type, and a note says which of the two takes the parameter as var.

    Kompiliert nicht
    fn look(body: (value: Int) => Void) {
      body 1
    }
    
    look { var number => number = 2 }
    // error: This parameter is `var` in the closure, but the function type takes it without `var`

What this is not#

A closure is not a block that runs inline. { print "hi" } on its own is a closure value, not a statement that prints; call it ({ print "hi" }()) or pass it somewhere that calls it.

fn runTwice(action: () => Void) {
  action()
  action()
}

runTwice { print "hi" }
Kompiliert nicht
{ print "hi" }
// error: This value is not used

The closure is built and then thrown away: an unused value is a compile error (see Bindings), and a closure is a value like any other.

A local fn is not a closure. It captures nothing and sees only its own parameters and the top level of its file (see Declaring a function); write a closure when the code has to reach into the surrounding scope.

Esc