The Problem
Consider fetching a user, then their profile, then their avatar URL. In imperative code you write three null checks. Miss one and you ship a bug. The checks are noise — they obscure what the code actually does.
The Core Idea
The Maybe monad wraps a value that might or might not exist. F# ships with this built-in: it's called Option<'T>. A value is either Some x or None. The monad pattern gives you a way to chain operations on that wrapped value without ever manually unwrapping and re-checking.
The two operations that make it a monad:
-
return— wrap a plain value:Some 42 -
bind(>>=) — apply a function that itself returns anOption, and flatten the result
let bind (opt: 'a option) (f: 'a -> 'b option) : 'b option =
match opt with
| None -> None
| Some x -> f x
If the input is None, the chain short-circuits immediately. If it's Some, the function runs and its result (which is itself an Option) becomes the next value in the chain.
A Runnable Example
Below is a self-contained pipeline: look up a user, get their config, read a specific key — any step can fail, and the types enforce that you handle it.
type User = { Name: string; ConfigId: int }
type Config = { Id: int; Settings: Map<string, string> }
let users = Map.ofList [ (1, { Name = "Alice"; ConfigId = 42 }) ]
let configs = Map.ofList [ (42, { Id = 42; Settings = Map.ofList [ ("theme", "dark") ] }) ]
let findUser id = Map.tryFind id users
let findConfig id = Map.tryFind id configs
let getSetting k cfg = Map.tryFind k cfg.Settings
// Chain with Option.bind — no manual null checks
let theme =
findUser 1
|> Option.bind (fun u -> findConfig u.ConfigId)
|> Option.bind (fun cfg -> getSetting "theme" cfg)
printfn "%A" theme // Some "dark"
let missing =
findUser 99
|> Option.bind (fun u -> findConfig u.ConfigId)
|> Option.bind (fun cfg -> getSetting "theme" cfg)
printfn "%A" missing // None
Why This Is a Monad
Three laws must hold (and they do for Option):
-
Left identity —
Some x |> Option.bind fequalsf x -
Right identity —
opt |> Option.bind Someequalsopt - Associativity — chaining two binds in sequence equals nesting them
You don't need to memorize these to use Option correctly, but knowing they hold means you can trust the composition — no surprises when you chain 10 steps deep.
F# Computation Expressions
F# lets you write the same pipeline in maybe { } style using computation expressions — syntactic sugar over bind. That's a natural next step once the raw mechanics are clear.
Key Takeaway
Option.bind is your null-check replacement. Every time you find yourself writing if result <> null then ..., ask whether an Option chain expresses the intent more clearly. It usually does.







