jonnifyfsharp

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.

C1.09.1 · part 1

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/Return methods.
let!
desugars to builder.Bind(e, fun x -> …) — not a keyword.
return
desugars to builder.Return(v).
C1.09.1 · part 2

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

THE BLOCK let! a = Some 3 return a + 1 → → THE CALLS builder.Bind(Some 3, fun a -> builder.Return(a + 1) ) Return is nested inside Bind's continuation builder.Bind(Some 3, fun a -> builder.Return(a + 1)) let! is Bind · return is Return
block let! a = Some 3 ; return a + 1 → builder.Bind(Some 3, fun a -> builder.Return(a + 1))

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

  1. let! a = Some 3Bind(Some 3, fun a ->
  2. let! b = Some 4Bind(Some 4, fun b ->
  3. return a + bReturn(a + b) ))
line 1 of 3 · let! a = Some 3 becomes builder.Bind(Some 3, fun a -> …)

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

Related in this course