Computations

Values vs. Computations

Following Paul Levy’s Call-By-Push-Value, Effekt distinguishes between values (such as 42, true, or instances of datatypes) and computations.

Examples of “computations” in Effekt are:

  • blocks (this is what we call functions),
  • instances of interfaces (also known as “objects”), and
  • regions

Functions (and all other computation types) are second-class in Effekt. To make this difference explicit, we pass values in parentheses (e.g. f(42)) and computations in braces (e.g. f { x => println(x) }).

def myMap[A, B](xs: List[A]) { f: A => B }: List[B] =
  xs match {
    case Nil() => Nil()
    case Cons(x, xs) => Cons(f(x), myMap(xs) { f })
  }

The block parameter f is enclosed in curly braces and syntactically separated from the value parameters. While multiple value parameters are separated by commas, multiple block parameters are expressed by enclosing each in curly braces.

[1, 2, 3].myMap { x => x * 2 }

In the previous example, the block passed to myMap did not use any effect operations and the signature of f in myMap did not mention any as well. What happens if the argument passed for f is effectful?

interface Yield[A] {
  def yield(x: A): Unit
}

def example(): List[Int] / Yield[Int] = [1, 2, 3].map { x => do yield(x); x * 2 }

The expanded type of myMap makes the effect positions explicit:

myMap[A, B](xs: List[A]) { f: A => B / {} }: List[B] / {}

The / {} on myMap itself means its caller needs to handle no extra effects. The / {} on f might look like it says “f must be pure”, but that is not the correct reading. Instead, it means that f imposes no requirements on myMap itself. Any effects f actually uses simply propagate to the call-site of myMap, where they are handled in the lexical scope where f was written.

This is contextual effect polymorphism: myMap stays effect-agnostic regardless of what f does. See Effect Polymorphism for more details.

Comparison

  Values (First-class) Computations (Second-class)
Term-level 42, "hello", true, Cons(1, Nil), box { [A](x: A) => x } { [A](x: A) => x }, new Exception, region r, unbox exc
  Literals, instances of datatypes and boxed computations Blocks, objects and regions
Type-level Int, String, Bool, List[Int], [A](A) => A at {} [A](A) => A, Exception, Region

On the term-level, boxing offers a way of transforming a second-class computation into a first-class value. Conversely, unboxing does the reverse and converts a first-class boxed computation back into a second-class computation. On the type-level, at {...} serves the same purpose as box. Further information can be found in the section about captures and objects.

References