Skip to content

grab expressions #316

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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