Dive C1.08.1
Option
A value that might not be there, made into an ordinary value you can hold and pass.
The type Option<'T> has exactly two shapes:
Some x when a value of type 'T is present, and
None when it is absent. It is F#'s replacement for null, and the
difference is that the compiler enforces it: to reach the x inside a
Some, a program must first account for the None. A function
that might not find an answer says so in its return type, and a caller cannot forget the missing case. This dive
covers the type, the library functions that produce it, and the three functions —
Option.map, Option.bind,
Option.defaultValue — that work with it without unwrapping by hand.
Some of 'T, or None
Option is a discriminated union with two cases. In the FSharp.Core library it is
declared as the two constructors below: Some wraps a value of the type parameter
'T; None takes nothing. Any function that might fail to
produce a result can return one instead of resorting to null.
type Option<'T> = | Some of 'T | None
A safe-division function shows the shape. Dividing by zero has no answer, so the function returns
None in that case and Some otherwise. The return type is
int option — the caller reads off the signature that a result might be absent.
let tryDivide a b = if b = 0 then None else Some (a / b) // val tryDivide : int -> int -> int option tryDivide 10 2 // val it : int option = Some 5 tryDivide 10 0 // val it : int option = None
- Option<'T>
- a union:
Some of 'Twhen a value is present,Nonewhen it is not. - int option
- the postfix way F# writes
Option<int>— anintthat might be missing. - None
- one value, shared across every
'T: the absence itself, not a null pointer. - vs. null
- the compiler forces both cases to be handled; a
Nonecannot be dereferenced by mistake.
Where options come from
The library follows a convention: a lookup that might not find anything is named with a
try prefix and returns an option.
List.tryFind returns the first element matching a predicate, or
None; Map.tryFind returns the value for a key, or
None. The absence is in the type, so a missing element is a value to handle, not an
exception to catch.
List.tryFind (fun x -> x > 3) [1; 2; 3; 4] // val it : int option = Some 4 let ages = Map.ofList [("a", 1); ("b", 2)] ages |> Map.tryFind "a" // val it : int option = Some 1 ages |> Map.tryFind "z" // val it : int option = None
try-prefixed library function returns an option;
its plainly-named sibling (List.find, Map.find) raises an
exception when the value is absent. The try form keeps the missing case inside the type
system. Reach for it when "not found" is a normal outcome rather than a bug.Explore tryDivide 10 d
Pick a divisor. The function tryDivide 10 d produces a
Some or a None; then
Option.map (fun x -> x + 1) applies + 1
only if the value is present. A None flows straight through
untouched — the step is skipped, and the result is still None. The diagram
carries the value along the Some track or stops it at the
None siding; the readout names the exact FSI value.
tryDivide 10 d · then Option.map (+1)
| divisor d | tryDivide 10 d | Option.map (+1) |
|---|---|---|
| 2 | Some 5 | Some 6 |
| 5 | Some 2 | Some 3 |
| 0 | None | None |
The missing case is handled once, by the type. Option.map runs the
transform on a Some and leaves a None alone — so no code
downstream has to ask "what if it was not there".
Threading without unwrapping
Three functions cover most work with options. Option.map applies a plain function
to the value inside a Some. Option.bind chains a function
that itself returns an option, so two failable steps compose without nesting
Some (Some _). Option.defaultValue ends the chain by
supplying a fallback for the None case, turning an
int option back into a plain int.
// map: apply a plain function, only if Some tryDivide 10 2 |> Option.map (fun x -> x + 1) // val it : int option = Some 6 // bind: chain a function that itself returns an option Some 6 |> Option.bind (fun x -> if x > 5 then Some (x * 2) else None) // val it : int option = Some 12 // defaultValue: supply a fallback, leaving plain int Option.defaultValue 0 None // val it : int = 0 Option.defaultValue 0 (Some 7) // val it : int = 7
The difference between map and bind is the shape of the
function each takes. map takes 'T -> 'U and lifts it over
the option; bind takes 'T -> 'U option — a step
that can itself fail — and flattens the result so the answer stays a single
option rather than an option of an option.
The idea
Absence is one of the values a function returns, named in its type — not a pointer that may be empty.
F# today
Some / None, read with match, threaded with map · bind · defaultValue.
Where this leads
Option says whether a value is there, but not why it is missing.
When a failure needs a reason — a parse error, a validation message —
Result keeps the same two-case shape and attaches detail to the failing side. Dive
C1.08.2 takes it up.
References
Sources
- Microsoft — F# documentation — options and the
try-prefixed library functions. - FSharp.Core API reference — the
Optionmodule:map,bind,defaultValue. - The F# Language Specification — the
optiontype and union pattern matching.
Related in this course
- C1.08.2 — Result — the same shape, with a reason on the failing side.
- C1.08 — Option, Result & railway-oriented errors — the module hub.
- Elixir — Purity — the BEAM equivalent: a missing value returned as
:error, never raised.