jonnifyfsharp

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.

C1.08.1 · part 1

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 'T when a value is present, None when it is not.
int option
the postfix way F# writes Option<int> — an int that 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 None cannot be dereferenced by mistake.
C1.08.1 · part 2

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
A 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)

10 / d tryDivide 10 d Option.map (+1) 10 b = 0 ? Some · / · None + 1 on Some only None map is skipped — None flows through 10
d = 2 · tryDivide 10 2 = Some 5 → Option.map (+1) = Some 6
Without JavaScript: the same three results, computed by hand
divisor dtryDivide 10 dOption.map (+1)
2Some 5Some 6
5Some 2Some 3
0NoneNone

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".

C1.08.1 · part 3

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

Related in this course