Pass-by-Value vs. Pass-by-Name

Parameters vs. Operands  vs. Arguments

Most programmers don’t distinguish between parameters, operands, and arguments, but making the distinction is important if you are writing interpreters and compilers.

In a sense all three refer to the inputs of a function.

Here's an example:

-> def foo = lambda(a, b) 2 * a + 3 * b
ok
-> foo(3 * 4, 6 – 1)
39

In this example a and b are the parameters. They are the names foo uses internally for its inputs.

3 * 4 and 6 – 1 are the operands. They are expressions. Their values: 12 and 5 are the arguments.

Thunks

A thunk is a parameterless closure, hence a value. It has an expression body and a pointer to its defining environment. We can think of a thunk as a frozen expression.

Jedi 2.1 has thunks. Here's an example:

-> def x = 5
ok
-> def promise1 = { def x = 10; freeze({ write("thawing"); 2 * x})
ok
-> promise1
thawing
20
-> promise1
20

Notes:

·        freeze is a new special form similar to lambda. It creates a thunk while lambda creates a closure.

·        Think of a thunk as a way of smuggling an expression into the value hierarchy by packaging it inside a value.

·        promise1 is a thunk created in an environment where x = 10

·        When a thunk is first referenced, its body is "thawed" i.e.,  executed. We see that this has happened above because the side effect doesn't happen until we reference promise1. (This requires a small modification to Identifier.execute: if an identifier is bound to a thunk, call it and return its value.)

·        When a thunk is referenced initially it caches its value. Subsequent references simply return the cached value rather than re-execute the body. We see this has happened above because the second time promise1 was referenced there was no side-effect.

Lazy vs. Eager Functions

An eager function executes all of its operands when it is called. For example, in Jedi 2.0:

case class FunctionCall(val operator: Identifier, val operands: List[Expression]) extends Expression:
   def execute(env: Environment): Value =
     val args: List[Value] = operands.map(_.execute(env)) // eager execution
      // etc.

Jedi 2.1 introduces a global flag that lets users choose between eager and lazy execution:

case class FunctionCall(val operator: Identifier, val operands: List[Expression]) extends Expression:
   def execute(env: Environment): Value =
     val args: List[Value] =
     if (flags.paramPassing == flags.BY_NAME && !env.contains(operator))
        (operands.map(freeze(_)) // lazy execution
        else
        (operands.map(_.execute(env)) // eager execution
      // etc.

 Notes:

·        By freezing operands we produce a list of thunk values rather than a list of different kinds of values.

·        Each thunk contains one un-executed operand. The thunk will be automatically "thawed" if it's encountered when executing the function's body.

·        The alu doesn't want thunks. If the operator isn't user-defined, we revert back to eager execution.

Demand-Driven Computing

We can see that lazy execution is being used in the following example:

-> def x = var(true)
ok
-> def blah = lambda(a, b) if ([x]) 2 * a else 2 * b
ok
-> blah(3 + 1, {write("thawing"); 3 * 4})
8

Notice that when we called blah we didn't see the side-effect that would've been produced by executing the second operand. Because [x] was true, blah didn't need to know the value of b so it wasn't computed. That's the point of pass-by-name: only compute an expression if it's needed. Only if there is a demand for it.

Other Examples

Languages like Haskel us lazy execution for function calls.

Scala's LazyLists: a #:: b doesn't execute b.

With pass-by-name we don't need special forms for short-circuit and conditional execution. Instead, these can be programmer-defined:

def and = lambda(a, b, c) if (a) a else if (b) b else c