OCaml from the ground up

When there is nothing to answer

Some and None make a missing answer part of the type, and Option.value, Option.map and match are the three ways to use one.

Part 9 of 12

Advanced18 min readBack to Courses

A missing answer, in the type

A function that may have nothing to give back says so in its type: it answers an option, which is either Some value or None. safe_div 12 0 answers None, and the compiler will not let the caller use it as a number, so the missing case has to be written down somewhere before the program runs.

That is the whole difference from a null value in languages that have one: an option is a variant with two constructors, so the usual match takes it apart, and there is no way to reach the number without handling the empty case. The compiler knows both constructors, so it can warn when a match forgets None, and it never lets the payload out without a branch that binds it.

Three ways to use one

Option.value ~default:0 turns an option into a plain value by saying what to use when it is None. Option.map changes the value inside a Some and leaves None alone, which is how a chain of transformations stays honest about the case where there was nothing to transform. A match handles both cases when each needs its own work.

The label ~default: is not optional at the call site, which is deliberate: the fallback is written next to the call, so a missing answer never quietly turns into a zero far from where the decision was made.

The example writes let show = function ..., which is shorthand for a one-argument function whose body matches on that argument. It is the same match you already know, with the argument left implicit.

An option is a variant you already know

'a option is a built-in variant with two constructors: None and Some of 'a. Some 3 has type int option, Some "hi" has type string option, and None can be either because it carries nothing. That is why a function returning an option is honest in its type: the caller cannot pretend the value is always there.

Option.map applies a function inside Some and leaves None alone, so it is the map you would write by hand. It does not flatten: mapping a function that itself returns an option gives option option, which is a signal that you need Option.bind or a match. Option.value ~default is the one-way door out of the option, and the label makes the fallback visible at the call site.

Pattern matching is still the general tool. Use Some x when the value is needed, None when the absence needs its own answer, and a match when both cases do different work. The shorthand function is only a match with the argument left implicit; either spelling is fine.

How an option is represented

None and Some 3 are ordinary variant values: a tag plus, in the Some case, the payload. There is no special null pointer and no box that can be unwrapped by force; Option.get exists, but it is a partial function that raises on None, which is why it is the one function in this chapter to avoid.

Because the representation is a variant, the cost is small and predictable: one word for the tag, plus the payload when there is one. A function returning int option does not allocate a wrapper around a number in the way a nullable reference might; it returns a value the match can inspect directly.

What goes wrong

The double option is the common one. Some (safe_div 1 0) is Some None, not None, and a later Option.value turns it into zero while the missing case is still there one layer down. If a function already returns an option, return its result directly; do not wrap it.

The other is a default that erases a real value. Option.value ~default:0 on a temperature reading makes a failed read look like zero degrees, and no later code can tell the difference. Choose the default only where the two cases genuinely mean the same thing, and keep the option everywhere else.

Three option mistakes: calling Option.get None, which raises instead of answering a default; wrapping an option again as Some (safe_div 1 0), which gives int option option and hides the missing case one layer deeper; and using Option.value ~default:0 where zero is a real value, so a missing answer becomes indistinguishable from a measured zero. Keep the option until the place where the fallback is genuinely meaningful.

Try these next

  • Write safe_head for a list and print both an empty and a non-empty result.
  • Map a function over a Some and a None and print both answers.
  • Look up three keys in an association list, two present and one missing, with List.assoc_opt.

Looking a key up in a small table

let lookup key table = List.assoc_opt key table

let () =
  match lookup "a" [ ("a", 1); ("b", 2) ] with
  | Some value -> print_endline (string_of_int value)
  | None -> print_endline "missing"

A division that can fail

Run it and read the four lines. Then write `safe_head` for a list that answers `None` when the list is empty, and print its result with `Option.value ~default:(-1)`.

Output
Click "Run" to execute your code

A phone book with missing names

Run it and read the lines. Then add a second number for `ada`, make the lookup return the first match, and print a name that is not in the book.

Output
Click "Run" to execute your code

Option.value ~default:0 found reads as the value, or 0. If both answers deserve to be visible, keep the option and match on it instead; the default is for the places where zero really is the right stand-in.

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What are the two values an option can have?