Options: No More null
Prerequisites: Pattern matching basics
Many languages use null for "no value", and forgetting to check for it
is one of the most common bugs in software. F# uses the option type
instead: a value is either Some x or None, and the type
(int option, string option) tells you it might be missing.
let tryDivide a b =
if b = 0 then None
else Some (a / b)
printfn "%A" (tryDivide 10 2)
printfn "%A" (tryDivide 10 0)
Some 5
None
Using an option
You can't use an int option as an int by accident — you have to
handle both cases. The clearest way is match:
let tryDivide a b = if b = 0 then None else Some (a / b)
let report a b =
match tryDivide a b with
| Some result -> printfn "%d / %d = %d" a b result
| None -> printfn "%d / %d: can't divide by zero" a b
report 10 2
report 10 0
10 / 2 = 5
10 / 0: can't divide by zero
The Option module
For common cases, the Option module is shorter than a match:
let price = Some 20
let missing: int option = None
printfn "%A" (price |> Option.map (fun p -> p * 2))
printfn "%A" (missing |> Option.map (fun p -> p * 2))
printfn "%d" (price |> Option.defaultValue 0)
printfn "%d" (missing |> Option.defaultValue 0)
printfn "%b %b" price.IsSome missing.IsNone
printfn "%A" (price |> Option.filter (fun p -> p > 50))
Some 40
None
20
0
true true
None
Option.maptransforms the value if there is one, and leavesNonealone.Option.defaultValueunwraps with a fallback.Option.bindchains steps that each return an option.
Options everywhere
The standard library returns options wherever something might not be found:
let tryParseInt (s: string) =
match System.Int32.TryParse s with
| true, n -> Some n
| false, _ -> None
let inputs = ["42"; "oops"; "7"]
let valid = inputs |> List.choose tryParseInt
printfn "%A" (inputs |> List.map tryParseInt)
printfn "%A" valid
printfn "%A" (List.tryHead ([]: int list))
printfn "%A" ([3; 9; 4] |> List.tryFind (fun n -> n > 5))
[Some 42; None; Some 7]
[42; 7]
None
Some 9
List.choose keeps only the Some values and unwraps them — perfect for
"parse what you can".
Common mistakes
- Using
.Valueon an option (price.Value) — it throws onNone, bringing back exactly the bug options prevent. UsematchorOption.defaultValue. - Returning
nullfrom your own F# functions — return an option instead. (You'll still meetnullfrom .NET libraries; Level 3 covers how to handle it.)
Practice
Write tryFindUser id that looks up an id in the list
[(1, "Ada"); (2, "Grace")] and returns an option, then print
"Hello, Ada" for id 1 and "Unknown user" for id 3.
Show solution
let users = [(1, "Ada"); (2, "Grace")]
let tryFindUser id =
users |> List.tryFind (fun (i, _) -> i = id) |> Option.map snd
for id in [1; 3] do
match tryFindUser id with
| Some name -> printfn "Hello, %s" name
| None -> printfn "Unknown user"
Hello, Ada
Unknown user