Skip to content
Computation Expressions
step 1/5

Reading — step 1 of 5

Learn

~1 min readActive Patterns and Computation Expressions

Computation expressions are F#'s general-purpose do-notation. Build a custom "effect" type, then write code that looks imperative but threads it through.

The built-in async { ... } is a computation expression. So is seq { ... }, query { ... }. You can define your own.

Minimal builder for Option:

type OptionBuilder() =
    member _.Return(x) = Some x
    member _.Bind(m, f) =
        match m with
        | Some v -> f v
        | None -> None

let option = OptionBuilder()

Use it — looks like sequential code:

let divide a b =
    if b = 0 then None else Some (a / b)

let compute () =
    option {
        let! x = divide 100 5      // 20
        let! y = divide x 2          // 10
        let! z = divide y 0          // None — short circuits!
        return z + 1
    }

compute ()    // None

let! is the unwrap operator — calls Bind under the hood. return calls Return.

More builder methods unlock more syntax:

  • Return(x) — for return
  • ReturnFrom(m) — for return!
  • Bind(m, f) — for let!
  • Combine(m1, m2) — for sequence of statements
  • Zero() — empty body
  • Delay(f) — defer evaluation
  • For(seq, body) — for loops
  • While(cond, body) — while loops
  • TryWith(body, handler) / TryFinally
  • Using(disposable, body)use!

The more you implement, the more like F# code your custom expression looks.

Real-world examples:

  • async — async/await
  • seq — generators
  • task (F# 6+) — interop with C# Task
  • Saturn / Giraffe — HTTP routing DSL
  • ZeroFormatter / FsLexYacc — parser combinators

Comparison to Haskell: F# computation expressions are ~equivalent to do-notation over monads. F# is more flexible (Combine, Zero — no monad laws enforced) and less mathy.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…