Fuzion Logo
fuzion-lang.dev — The Fuzion Language Portal
JavaScript seems to be disabled. Functionality is limited.

Lazy Evaluation of Arguments

Lazy evaluation can provide improved performance and allow code that would otherwise not terminate or cause errors. Lazy evaluation can be achieved using a nullary function argument.

Lazy evaluation in other languages

Haskell

Lazy evaluation is the default for all function arguments.

Java

Java has built-in lazy evaluation for binary boolean operators && and ||. This permits code such as


if (a != null && a.toString().equals("a"))
  {
    System.out.prinltn("a is a");
  }

if (b == null || !b.toString().equals("b"))
  {
    System.out.prinltn("b is not b");
  }

This code would crash with a NullPointerException if the evaluation of the right hand side of the boolean operators would not be lazy.

Rust

Rust has a macro lazy! to create a memoized lazily evaluated value. Evaluation is explicit using *, see the following code taken from docs.rs:


fn expensive() -> i32 {
    println!("I am expensive to evaluate!"); 7
}

fn main() {
    let a = lazy!(expensive()); // Nothing is printed.

    // Thunks are just smart pointers!
    assert_eq!(*a, 7); // "I am expensive to evaluate!" is printed here

    let b = [*a, *a]; // Nothing is printed.
    assert_eq!(b, [7, 7]);
}

Apart from this, Rust seems to have no specific support for lazy evaluation of arguments, functions with lazy arguments need to either use function types or lazy_st::Thunk.

F#

A lazily evaluatable expression in Rust is declared using lazy (expr), evaluation is done via a call to Force on the expression. The type of a lazy expression created from an expression of type T is Lazy<T>.

Here is an example taken from microsoft.com:


let x = 10
let result = lazy (x + 10)
printfn "%d" (result.Force())

Some developers are unhappy about F#'s lazy evaluation not being transparent and not memoized.

C#/.NET

A clazz Lazy<T> provides lazy evaluation and result caching in conjunction with specifc thread safety modes.

Syntax alternatives for Fuzion

Let's play around with syntax alternatives using a feature and that implements boolean conjunction, a feature list that creates a list from a head and a lazy tail, and a lazy type_of function:

Approach
Aspect
Explicit lambda Implicit lambda with lazy keyword Implicit lambda lazy feature
Idea

Explicitly use function types, i.e., add ()-> at declaration and call, add () at use of lazy argument.

A new keyword lazy instructs the front end to produce a unary function type, arguments get wrapped automatically on a call and called when used.

Use explicit function types at declaration and use, but automatically wrap incompatible actual arguments in a unary function on use. This is a special case of partial application with no arguments applied and no arguments remaining.

Make lazy a feature of the base library that inherits from or wraps the unary function type. Add syntax sugar that automatically wraps arguments in an instance of lazy and automatically calls them when used.

Feature declaration

and(a bool, b ()->bool) bool =>
  if a
    b()
  else
    false


list(T type, h T, t () -> list T) list T =>
  ref : Cons T (list T)
    head => h
    tail => t()


type_of(T type, _ ()->T) => T
          

and(a bool, lazy b bool) bool =>
  if a
    b
  else
    false


list(T type, h T, lazy t list T) list T =>
  ref : Cons T (list T)
    head => h
    tail => t


type_of(T type, lazy _ T) => T
          

and(a bool, b ()->bool) bool =>
  if a
    b()
  else
    false


list(T type, h T, t () -> list T) list T =>
  ref : Cons T (list T)
    head => h
    tail => t()


type_of(T type, _ ()->T) => T
          

and(a bool, b lazy bool) bool =>
  if a
    b
  else
    false


list(T type, h T, t lazy list T) list T =>
  ref : Cons T (list T)
    head => h
    tail => t


type_of(T type, _ lazy T) => T
          
Feature call

c := and d ()->e


ones => list 1 ()->ones


t := type_of ()->(3, 4.0)   # tuple i32 f64
          

c := and d e


ones => list 1 ones


t := type_of (3, 4.0)   # tuple i32 f64
          

c := and d e


ones => list 1 ones


t := type_of (3, 4.0)   # tuple i32 f64
          

c := and d e


ones => list 1 ones


t := type_of (3, 4.0)   # tuple i32 f64
          
Comment

The developer is explicit at the feature declaration and at the call site.

For a call to and the second argument e gets wrapped in ()->( and ) automatically.

The caller of and would get e wrapped in a lambda automatically since the result of e is not compatible to b:

Maybe the most beautiful solution?

Need to decide to either use a value type around a function


lazy(T type, f ()->T) is ...
          

or a ref type inheriting form Function as in


Lazy(T type) ref : Function T is ...
          
Pros&Cons

🟡 explicit but cryptic at declaration b ()->bool
❌ verbose at use b()
❌ verbose at call ()->e
✅ explicit at use b()
✅ explicit at call ()->e
✅ no extra syntax sugar
✅ no extra keyword
type_of cumbersome.
❌ memoization only if unary function type provides this (which is hard in case of effects)

✅ explicit at declaration lazy b bool
✅ concise at use b
✅ concise at call e
🟡 implicit at use
🟡 implicit at call
❌ extra syntax sugar on declaration, use and call
❌ addition keyword lazy
type_of works as expected
❌ memoization only if unary function type provides this (which is hard in case of effects)

🟡 explicit but cryptic at declaration b ()->bool
✅ concise at use b
✅ concise at call e
🟡 implicit at use
❌ implicit black magic at call
✅ same syntax sugar as for partial application
✅ no extra keyword
type_of does not work for nullary functions: type_of ()->3 would be i32
❌ memoization only if unary function type provides this (which is hard in case of effects)

✅ explicit at declaration b lazy bool
✅ concise at use b
✅ concise at call e
🟡 implicit at use
🟡 implicit at call
🟡 extra syntax sugar on use and call
✅ no extra keyword
🟡 type_of applied to lazy value is the wrapped type: x lazy i32 := 3; type_of x would be i32
lazy could implement memoization

last changed: 2024-06-28