Dive C1.09.1
Computation expressions
Inside a { } block, let! and return are method calls, not keywords.
A computation expression is the something { ... } syntax.
The name in front — seq, async,
option — is a builder object, and the braces hold code that the
compiler rewrites into method calls on it. let! x = e becomes
builder.Bind(e, fun x -> …); return v becomes
builder.Return(v). There is no special runtime: the block is ordinary F# the moment the
compiler finishes the rewrite. The built-in seq { } is a computation expression you
have already used.
A computation expression you know
The seq { } block builds a lazy sequence. It looks like new syntax, but it is a
computation expression: seq is a builder, and the
for … -> … inside the braces is rewritten into method calls on it. Piping the
result through Seq.toList forces it and shows the values.
seq { for x in 1..3 -> x * x } |> Seq.toList // val it : int list = [1; 4; 9]
The same shape covers absence, failure, and asynchronous work — change the builder in front of the braces
and the meaning of let! and return changes with it. The
next part shows the rewrite that makes this work.
- computation expression
- the
builder { ... }block syntax. - builder
- the object named in front of the braces; it carries the
Bind/Returnmethods. - let!
- desugars to
builder.Bind(e, fun x -> …)— not a keyword. - return
- desugars to
builder.Return(v).
The block is method calls
Take a one-bind block: option { let! a = Some 3 ; return a + 1 }. The compiler
rewrites it into a call to Bind whose continuation calls
Return. The diagram pairs each line of the block with the call it becomes.
option { let! a = Some 3 ; return a + 1 } · the block becomes a call tree
There is no magic in the braces. let! is a Bind
call and return is a Return call — the builder defines what
each one does.
Desugaring, line by line
Reveal the mapping one line at a time. The block on the left is a two-bind
option { … }; each step pairs a source line with the
Bind or Return call it becomes. Both columns are present
from the start — the steps highlight the pairing in turn.
option { let! a = Some 3 ; let! b = Some 4 ; return a + b } · each line to its call
- let! a = Some 3Bind(Some 3, fun a ->
- let! b = Some 4Bind(Some 4, fun b ->
- return a + bReturn(a + b) ))
The whole block flattens to one nested Bind/Return
tree. Read it top to bottom and the rewrite is mechanical — the same for any builder.
The block syntax
A readable builder { ... } block with let! and return.
What the compiler emits
Nested calls to Bind and Return on the builder you named.
Where this leads
One rewrite, many builders. async uses the same Bind shape
to sequence asynchronous work, and a builder you write yourself can give let! any
meaning at all. Dive C1.09.2 takes the rewrite to async { }; the lab in C1.09.3 has
you write the builder from scratch.
References
Sources
- Microsoft — F# documentation — computation expressions and the builder methods
BindandReturn. - The F# Language Specification — the desugaring rules for
let!andreturn. - FSharp.Core API reference —
Seq.toListand the sequence builder.
Related in this course
- C1.09.2 — Async workflows — the same rewrite, applied to asynchronous work.
- C1.09 — Computation expressions — the module hub and its three dives.
- Elixir — Pipeline lab — the BEAM course's functional lab, building chains by hand.