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.
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.
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.
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.
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