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
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 evalThe computation only runs when .eval() is called.
Memoization
Lazy values are computed only once. Subsequent evaluations return the cached result:
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():
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:
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:
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:
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:
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
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
| Aspect | Haskell | Nic |
|---|---|---|
| Default | Lazy | Strict |
| Lazy values | Implicit | Explicit Lazy[T] |
| Forcing | Implicit (pattern match) | Explicit .eval() |
| Memoization | Built-in | Built-in |
Summary
| Feature | Syntax |
|---|---|
| Create lazy | lazy { return expression; } |
| Type | Lazy[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.