Authoritative source: Explicit laziness

Explicit laziness

tulam evaluates ordinary values strictly. Laziness is an explicit type and operation, so delayed computation cannot be confused with an already available value.

Implementation status: The following forms are normative, but memoized call-by-need is not implemented end to end on the native reference backend.

Lazy values

primitive Lazy(a:Type);
function delay(body:Unit -> a) : Lazy(a) = intrinsic;
function force(item:Lazy(a)) : a = intrinsic;

Lazy(a) is distinct from a. There is no implicit automatic force. Evaluation is memoized call-by-need: after successful forcing, later forces share the result.

Expression sugar

~expression

desugars to:

delay(fn() = expression)

~ makes the delay visible at the construction point.

Lazy fields and binders

type Stream(a:Type) =
    Empty
  + More * head:a * ~tail:Stream(a);

~tail:T means tail:Lazy(T). A lazy function binder similarly binds a Lazy(T) value that must be forced explicitly.

function streamHead[a:Type](~xs:Stream(a)) : Maybe(a) =
    match force(xs)
    | Empty -> Nothing
    | More * head * tail -> Just(head);

Effects and suspension

Pure Lazy(a) must not silently capture an effectful computation. Effectful suspension uses an effect-indexed library type so the capability requirements remain in the type.

Why explicit?

Explicit laziness keeps default evaluation and cost predictable, makes sharing observable in the type, and avoids a backend deciding independently which expressions are semantically delayed. Optimization may still avoid unnecessary work when it preserves strict semantics.

Common mistakes

Do not expect implicit forcing, use pure Lazy(a) to hide effects, or rely on the specified memoization behavior on native before its implementation gate is complete.

Recap

Ordinary tulam is strict. Lazy(a), delay, force, and ~ make suspension and sharing an explicit part of a program’s contract.

Normative details: Language Reference §15.