Skip to content
AgileLogics
6 min read

Computation Expressions

You've already used several computation expressions: seq { }, list expressions, and (soon) async { } and task { }. They all share one syntax — a builder name followed by a block — and a set of special keywords whose meaning the builder defines:

Keyword Meaning inside the block
let! "unwrap" a wrapped value (await a task, take a Some, ...)
do! like let! for a step that returns nothing useful
return wrap a plain value as the result
return! return an already-wrapped value
yield / yield! produce items (sequences and lists)
use! let! plus automatic disposal
match! let! followed by match

The idea: you write straight-line code, and the builder handles the plumbing — laziness for seq, waiting for task, early exit for option or result.

The same code, with and without

Suppose three lookups can each fail by returning None:

let users = Map.ofList [ 1, "ada"; 2, "grace" ]
let emails = Map.ofList [ ("ada", "ada@example.com") ]
let domains = Map.ofList [ ("example.com", "Example Inc.") ]

let domainOf (email: string) = email.Split('@') |> Array.tryItem 1

// Nested matches: correct but hard to read
let companyNested id =
    match Map.tryFind id users with
    | None -> None
    | Some user ->
        match Map.tryFind user emails with
        | None -> None
        | Some email ->
            match domainOf email with
            | None -> None
            | Some domain -> Map.tryFind domain domains

// Option.bind: flatter
let companyBind id =
    Map.tryFind id users
    |> Option.bind (fun u -> Map.tryFind u emails)
    |> Option.bind domainOf
    |> Option.bind (fun d -> Map.tryFind d domains)

printfn "%A %A" (companyNested 1) (companyBind 1)
printfn "%A %A" (companyNested 2) (companyBind 2)
Some "Example Inc." Some "Example Inc."
None None

With an option computation expression (you'll write the builder in the next lesson), the same logic reads like ordinary code:

let company id =
    option {
        let! user = Map.tryFind id users
        let! email = Map.tryFind user emails
        let! domain = domainOf email
        return! Map.tryFind domain domains
    }

Each let! means "if this is Some x, continue with x; if it's None, stop and return None".

Built-in builders

  • seq { }, [ ], [| |] — sequences, lists, arrays
  • async { } — F# asynchronous workflows
  • task { } — .NET Tasks, for interop and high-performance I/O
  • lazy ( ) isn't a builder, but is related
  • query { } — LINQ-style queries, used with databases
let evensSquared =
    [ for i in 1 .. 10 do
        if i % 2 = 0 then
            let sq = i * i
            yield sq ]

let q =
    query {
        for n in [ 5; 3; 8; 1 ] do
        where (n > 2)
        sortBy n
        select (n * 10)
    }

printfn "%A" evensSquared
printfn "%A" (List.ofSeq q)
[4; 16; 36; 64; 100]
[30; 50; 80]

Libraries add many more: result { } and validation { } from FsToolkit.ErrorHandling, html { }-style builders for web views, cancellableTask { }, and so on.