Skip to content

grab expressions #316

@bvssvni

Description

@bvssvni

A grab expression computes a value inside a closure that depends on variables in the closure environment. It can also be used to precompute data at top level (more about this later).

Example:

fn main() {
    a := sift i 3 { \(x) = x + grab i } // Compute the value of `i` inside closure
    for i {
        println(\a[i](0))
    }
}

A grab expression is evaluated when the closure is created and inserted into the AST of the closure variable. You can use any expression, as long as it returns a value:

grab i * 5 + 2 // OK
grab println("hi") // ERROR: Expected something, found `void`

It can not depend on any variable of the closure itself. Variables with same name will be reported as an error:

x := 2
a := \(x) = x + grab x // ERROR: Grabbed `x` has same name as closure variable

You can print out a closure to view the result of grab expression:

fn main() {
    a := \(x: f64) = x + grab prod i 3 { i + 1 }
    println(a) // prints `\(x: f64) = x + 6`
    println(\a(0)) // prints `6`
}

An error is shown if you try to use a grab expression outside a closure:

fn main() {
    a := grab 1 + 1
    println(a)
}
 --- ERROR --- 
main (source/test.dyon)

`grab` expressions must be inside a closure
2,15:     a := grab 2
2,15:               ^

Grabbing higher levels

A closure inside a closure can contain a grab expression that computes values 1, 2 or higher levels:

a := \(x) = \(y) = \(z) = z + (grab y) + (grab '2 x)

Remember to use parenthesis (grab <expr>) if you are using multiple grab statements within same expression.

Thumb rule: The grab expression with highest level gets evaluated first.

Evaluation order

Blocks are evaluated after higher level expressions, such as in if expressions, where the conditions are evaluated before blocks:

fn main() {
    a := 5
    b := \(x) = if x {
        grab {
            println("two")
            a
        }
    } else if x < grab {
        println("one")
        a - 2
    } {
        grab {
            println("three")
            a + 1
        }
    }
    // one
    // two
    // three
    // prints `\(x: any) = if x { 5 } else if x < 3 { 6 }`
    println(b)
}

Otherwise, the evaluation follows top-down as you normally expect the code to be executed.

Precomputing data at top level

One can also use grab to precompute at top level. This is often used when the script is called frequently, e.g. to generate audio.

fn main() {
     a := grab [1, 2, 3]  // Will be precomputed when loading script
}

The following data can be precomputed:

  • Array fill, e.g. [false; 24]
  • Array, e.g. [1, 2, 3]
  • Object, e.g. {a: 1, b: 2}
  • 4D vector, e.g. (1, 2, 3, 4)
  • Link structure, e.g. link {1 "hi" false}

The data structures can be nested, e.g. [[1, 2], [3, 4]] or [{a: 1, b: 3}, {a: 2, b: 4}].

Since the grab expression at top level injects the variable into the AST, when you mutate the variable Dyon will use copy-on-write. This means that grab expressions at top level is slower when mutating the variable, but faster when only reading from it.

Motivation

Adds a non-state capture ability of variables in the environment of closures. It is more powerful in the way that it works as a partial evaluation operator, but less flexible than mutable captured variables.

Dyon uses current objects for mutating the environment, so there is less need for capturing mutatable variables. A non-state capture is easier to reason about, requires no lifetime, and does not add overhead when calling closures.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions