Skip to content

Lazy Functions

Bill Hails edited this page May 10, 2026 · 4 revisions

Lazy functions are functions that take their arguments unevaluated (actually wrapped in thunks) and evaluate them only on-demand.

For example in the preamble we now have:

lazy fn AND(a, b) { if (a) { b } else { false } }
lazy operator "_and_" left 3 AND;

And similarly for the other logical operators. Note this preserves the short circuiting of the and, e.g.

let
    fn a() { print "a called"; false }
    fn b() { print "b called"; true }
    fn c() { print "c called"; true }
in
    a() and b() or c();

prints:

a called
c called

How this works is, the macro is parsed and converted to a function and a note made in the compile-time environment that it is a macro. Then if it is invoked later, each of it's arguments is wrapped in a thunk before passing them to the function. Likewise within the macro body, any reference to a formal argument a is replaced with a call to that argument a(). To return to the AND example from above

lazy fn AND(a, b) { if (a) { b } else { false } }

is rewritten to

fn AND(a, b) { if (a()) { b() } else { false } }

and likewise any call to AND(a(), b()) becomes AND(fn () {a()}, fn () {b()}) which a subsequent generic optimization step will transform to AND(a, b), because fn () { a () } can be reduced to just a. In summary

AND(a(), b()) => if (a()) { b() } else { false }

Which is just what is required.

Thunking and Forcing

As part of the macro work, we've finally added a true thunk type to the type system, so a function of no arguments like fn () { 10 } no longer has the type number but instead has the type #() -> number (thunk of number). Macros like AND above gain correct types, so AND is (#() -> bool) -> (#() -> bool) -> bool.

While macros automatically have their arguments thunked and then forced (applied) at the point of use, other functions can benefit from having lazy arguments. For that reason there are a couple of new operators defined in the preamble: prefix & wraps its argument in a thunk, and prefix * forces a thunk. There is no real value to these operators other than being slightly easier to type than the equivalent fn () { x } and x().

Next: Macros

Clone this wiki locally