Skip to content
AgileLogics
6 min read

Sequence Expressions and Laziness

Prerequisites: Arrays and sequences

You've seen seq { ... } briefly. It's a sequence expression: code that produces values on demand with yield.

yield and yield!

let countdown n =
    seq {
        for i in n .. -1 .. 1 do
            yield i
        yield 0
    }

let withHeader items =
    seq {
        yield "--- start ---"
        yield! items
        yield "--- end ---"
    }

printfn "%A" (countdown 3 |> Seq.toList)
withHeader ["a"; "b"] |> Seq.iter (printfn "%s")
[3; 2; 1; 0]
--- start ---
a
b
--- end ---

yield! ("yield bang") yields every item of another sequence.

Infinite and recursive sequences

let rec fibsFrom a b =
    seq {
        yield a
        yield! fibsFrom b (a + b)
    }

let fibonacci = fibsFrom 0L 1L

printfn "%A" (fibonacci |> Seq.take 10 |> Seq.toList)
printfn "%d" (fibonacci |> Seq.find (fun n -> n > 1000L))

let primes =
    Seq.initInfinite id
    |> Seq.filter (fun n -> n > 1 && Seq.forall (fun d -> n % d <> 0) (seq { 2 .. int (sqrt (float n)) }))

printfn "%A" (primes |> Seq.take 10 |> Seq.toList)
[0L; 1L; 1L; 2L; 3L; 5L; 8L; 13L; 21L; 34L]
1597
[2; 3; 5; 7; 11; 13; 17; 19; 23; 29]

Only the items you ask for (Seq.take 10) are ever computed.

Laziness has a catch

A sequence is a recipe, not a stored result — every time you iterate it, it runs again:

let numbers =
    seq {
        for i in 1 .. 3 do
            printfn "  computing %d" i
            yield i
    }

printfn "sum = %d" (Seq.sum numbers)
printfn "max = %d" (Seq.max numbers)

let cached = numbers |> Seq.cache
printfn "cached sum = %d" (Seq.sum cached)
printfn "cached max = %d" (Seq.max cached)
  computing 1
  computing 2
  computing 3
sum = 6
  computing 1
  computing 2
  computing 3
max = 3
  computing 1
  computing 2
  computing 3
cached sum = 6
cached max = 3

Use Seq.cache, or convert to a list or array (Seq.toList), when a sequence is expensive and used more than once.

lazy values

lazy delays a single computation until .Force() (or .Value) is called, and remembers the result:

let expensive =
    lazy (
        printfn "calculating..."
        42
    )

printfn "before"
printfn "%d" (expensive.Force())
printfn "%d" (expensive.Force())
before
calculating...
42
42

Streaming data

Laziness is what lets you process a huge file one line at a time: File.ReadLines path |> Seq.filter ... |> Seq.length never holds the whole file in memory. You'll use this in the files lesson.