Skip to content

Lazy Evaluation

Lazy evaluation allows deferring computation until the result is actually needed. An expression wrapped in lazy is not evaluated immediately — instead, it creates a thunk (a deferred computation). The thunk is only evaluated when force is called on it, and the result is cached (memoized) so subsequent force calls return the cached value without re-evaluation.

12.1 Overview

Concept Description
lazy expr Creates a deferred computation
force v Evaluates the thunk and caches the result
Memoization Once forced, the result is cached permanently
Captures Variables from enclosing scope are captured at creation time

Use cases: - Expensive computations that may not always be needed - Conditional evaluation — avoid computing values that are only used in certain branches - Caching — compute once, use many times

12.2 Creating Lazy Values

# Simple lazy value
let x = lazy 42

# Lazy computation
let expensive = lazy (computeHeavyResult data)

# Lazy values capture variables from enclosing scope
let base = 100
let multiplier = 3
let computed = lazy (base * multiplier)

The expression after lazy is not evaluated at binding time. It is stored as a thunk along with any captured variables from the enclosing scope.

12.3 Forcing Lazy Values

Use force to evaluate a lazy value:

# Direct force
let x = lazy (1 + 2)
println (force x)          # => 3

# Pipeline force
let x = lazy 42
x |> force |> println      # => 42

12.4 Memoization Semantics

The first force evaluates the deferred expression and caches the result. Subsequent force calls return the cached value without re-evaluation:

let x = lazy (1 + 2)
let a = force x          # Evaluates the expression → 3
let b = force x          # Returns cached result → 3 (no re-evaluation)
# a == b == 3

12.5 Captures and Closures

Variables referenced in the lazy body are captured at creation time:

let a = 10
let b = 20
let x = lazy (a + b)
println (force x)           # => 30

Captured values are copies taken at the point where lazy is evaluated. Later changes to the original bindings do not affect the lazy value.

12.6 Practical Examples

# Avoid expensive computation when not needed
let config = lazy (parseConfig (cat "config.yml"))
if needsConfig then
    let c = force config
    applyConfig c

# Multiple lazy values with conditional forcing
let data = lazy (fetchRemoteData url)
let cachedData = lazy (readCache cacheFile)

let result =
    if cacheExists cacheFile
    then force cachedData
    else force data

12.7 Lazy Sequences

Lazy sequences (seq) build on lazy evaluation to create sequences whose elements are computed on demand. Each yield produces an element, and yield! splices in another sequence.

Creating Sequences

# Basic sequence
let s = seq { yield 1; yield 2; yield 3 }

# Multi-line sequence
let s = seq {
    yield 10
    yield 20
    yield 30
}

# Empty sequence
let empty = seq {}

yield! (Splice)

Use yield! to splice another sequence at the current position:

let rest = seq { yield 3; yield 4 }
let all = seq { yield 1; yield 2; yield! rest }
# all evaluates to: 1, 2, 3, 4

Converting to Lists

Lazy sequences are not eagerly evaluated. Use toList to force evaluation into a list:

let s = seq { yield 1; yield 2; yield 3 }
s |> toList |> println       # => [1; 2; 3]

Seq-Aware Operations

The following operations work directly on sequences:

Operation Description
take n seq Takes the first N elements, returns a list
each f seq Applies f to each element for side effects
toList seq Forces the entire sequence into a list
for x in seq do ... done Iterates over sequence elements
let s = seq { yield 1; yield 2; yield 3; yield 4; yield 5 }

# Take first 3 elements
s |> take 3 |> println              # => [1; 2; 3]

# Iterate with each
s |> each println
# => 1
# => 2
# => 3
# => 4
# => 5

# Convert to list, then use list operations
s |> toList |> map (fun x -> x * 2) |> println   # => [2; 4; 6; 8; 10]

See also: Variables & Bindings | Functions | Error Handling