Skip to content

Lazy Evaluation

Nic is strict by default—expressions are evaluated immediately. When you need deferred evaluation, use explicit lazy { } blocks with return statements.

Basic Laziness

nic
fn expensive() -> i32 {
    println("Computing...");
    return 42;
}

fn main() -> unit {
    // Create lazy value (not evaluated yet)
    // Type is inferred as Lazy[i32]
    let lazy_val = lazy { return expensive(); };
    
    println("Before eval");
    
    // Force evaluation
    let result: i32 = lazy_val.eval();
    
    println("After eval");
    return;
}

Output:

Before eval
Computing...
After eval

The computation only runs when .eval() is called.

Memoization

Lazy values are computed only once. Subsequent evaluations return the cached result:

nic
fn main() -> unit {
    let lazy_val = lazy { 
        println("Computing!");
        return 100;
    };
    
    let a: i32 = lazy_val.eval();  // prints "Computing!"
    let b: i32 = lazy_val.eval();  // no print, returns cached 100
    let c: i32 = lazy_val.eval();  // no print, returns cached 100
    
    return;
}

The force Function

Alternative to .eval():

nic
fn main() -> unit {
    let lazy_val = lazy { return 42; };
    
    let a: i32 = lazy_val.eval();  // method syntax
    let b: i32 = force(lazy_val);  // function syntax
    
    return;
}

Lazy Struct Fields

Defer expensive field initialization:

nic
struct Config {
    name: string,
    data: Lazy[string]
}

fn load_data() -> string {
    println("Loading from disk...");
    return "large dataset";
}

fn main() -> unit {
    let cfg: Config = Config{
        name: "app",
        data: lazy { return load_data(); }
    };
    
    println(cfg.name);  // "app" - no loading yet
    
    // Data loaded only when accessed
    println(cfg.data.eval());
    
    return;
}

No Implicit Forcing

Lazy[T] and T are different types. You must explicitly force:

nic
fn use_int(n: i32) -> unit {
    println("got int");
    return;
}

fn main() -> unit {
    let lazy_num = lazy { return 42; };
    
    // use_int(lazy_num);        // ERROR: type mismatch
    use_int(lazy_num.eval());    // OK: explicitly forced
    
    return;
}

Lazy for Conditional Computation

Avoid unnecessary work:

nic
fn expensive_default() -> i32 {
    println("Computing default...");
    return 999;
}

fn get_value(use_default: bool) -> i32 {
    let default = lazy { return expensive_default(); };
    
    if use_default {
        return default.eval();
    } else {
        return 42;  // default never computed
    }
}

Infinite Streams

Build infinite data structures with lazy tails:

nic
enum Stream[T] {
    Nil,
    Cons(T, Lazy[Stream[T]])
}

fn integers_from(n: i32) -> Stream[i32] {
    return Cons(n, lazy { return integers_from(n + 1); });
}

fn take[T](n: i32, s: Stream[T]) -> *List[T] {
    if n <= 0 {
        return new Nil;
    }
    return match s {
        Nil -> new Nil,
        Cons(x, rest) -> new Cons(x, take(n - 1, rest.eval()))
    };
}

fn main() -> unit {
    // Infinite stream: 1, 2, 3, 4, ...
    let naturals: Stream[i32] = integers_from(1);
    
    // Take only what we need
    let first_ten: *List[i32] = take(10, naturals);
    
    return;
}

Lazy Fibonacci

nic
fn fibs_from(a: i32, b: i32) -> Stream[i32] {
    return Cons(a, lazy { return fibs_from(b, a + b); });
}

fn fibonacci() -> Stream[i32] {
    return fibs_from(0, 1);
}

fn main() -> unit {
    let fibs: Stream[i32] = fibonacci();
    
    // Get 20th Fibonacci number without computing all previous
    // (though in practice, this implementation does compute them)
    let first_20: *List[i32] = take(20, fibs);
    
    return;
}

When to Use Lazy

Good uses:

  • Expensive computations that might not be needed
  • Infinite data structures
  • Breaking circular dependencies
  • Memoizing results

Avoid when:

  • Simple values (overhead not worth it)
  • Values always needed (just compute eagerly)
  • Side effects that must happen immediately

Comparison with Haskell

AspectHaskellNic
DefaultLazyStrict
Lazy valuesImplicitExplicit Lazy[T]
ForcingImplicit (pattern match)Explicit .eval()
MemoizationBuilt-inBuilt-in

Summary

FeatureSyntax
Create lazylazy { return expression; }
TypeLazy[T]
Force (method)lazy_val.eval()
Force (function)force(lazy_val)

What's Next?

Now that you understand lazy evaluation, you're ready for Nic's most innovative feature: Temporal Zippers. Temporal zippers model all state changes as navigation through time, giving you pure functional semantics with zero-cost abstractions and built-in time-travel debugging.

Released under the MIT License.