Functional Programmingbeginner11 min

The Maybe-Value

A labelled box that's either holding something or clearly empty — so "nothing's here" can never sneak up and crash your program.

Imagine two parcels on your doorstep. One is a plain sealed box with no label — you have no idea if there's a gift inside or just packing air, and you only find out when you tear it open. The other has a clear window: you can see at a glance whether it's holding something or sitting empty.

Most code today uses the first kind of box for "no value" — and it has a name. The maybe-value is the second kind: a box that's honest about whether it's full or empty, before you ever reach inside.

The billion-dollar mistake

For decades, the standard way to say "there's nothing here" was null. A function that might not find an answer hands you back null, and everything looks fine — until some later line tries to actually use it. Then your program blows up with a crash, often far away from where the empty value first appeared.

The deep problem is that null is invisible. Nothing in the type tells you a value might be missing, so it's terribly easy to forget the empty case and discover it the hard way, in production, at 2am.

Here's how that plays out in F#. An order page prints a shipping label using three tiny functions, each calling the next. The user comes from a JSON row in the database, and Ada never saved an address. Step through it, and predict where the program crashes before you look.

Watch out

The inventor of null later called it his "billion-dollar mistake." Why so costly? Because the absence is hidden: "no value" looks exactly like a real value right up until your code touches it — and the crash report points at the line that touched it, not the place where the value went missing. A whole category of bugs comes from this one quiet little nothing.

Tip

Where nulls still sneak into F#. F#'s own types don't use null in everyday code, but values that arrive from outside — JSON, databases, .NET libraries — can. Check them once, at the boundary where they enter your program, and turn them into options there. For .NET values such as strings, Option.ofObj does exactly that: null becomes None, anything else becomes Some.

How it works

The maybe-value fixes this by making absence visible. Instead of a sealed box that might secretly be empty, you get a clearly labelled box with exactly two states. Either it's Some x — it's holding a value, and there it is — or it's None — it's plainly empty, and everyone can see that.

The magic is that this lives in the type. The moment a value might be missing, the type says so out loud, and the compiler insists you deal with both cases instead of forgetting one.

Step through the same two users again, this time with Address declared as an Address option. The old cityOf hasn't changed yet: predict what happens when you build it.

That's the whole trade. The compiler won't let you treat a box as if it were the thing inside, so you can't forget the empty case — and in return, this value can never surprise you with a crash again.

The same honesty works for any lookup. Here's one that may or may not find a number:

// The `: int option` part is the honest label on the box
let tryFind (key: string) (table: Map<string, int>) : int option =
    Map.tryFind key table

tryFind "answer" myTable   // Some 42  — found it, here it is
tryFind "missing" myTable  // None     — clearly empty

That int option in the signature is the point: anyone reading it knows, before running a thing, that an answer might not come back. The absence is part of the contract, not a nasty surprise.

Check yourself

A function returns a string option. A teammate passes its result straight to code that expects a plain string. What happens?

Working without opening the box

Here's the lovely part: you can often transform what's inside the box without unpacking it first. Option.map says "do this to the value if there is one; otherwise just leave the empty box empty." You never have to write a nervous "but what if it's missing?" check yourself.

let doubled = Option.map (fun x -> x * 2)

doubled (Some 21)   // Some 42  — the value inside got doubled
doubled None        // None     — nothing to do, stays empty

Notice the empty case took care of itself. The doubling only happened where there was something to double, and the None sailed straight through.

Eventually you'll want a plain value back, not a box. When the box is empty, Option.defaultValue lets you supply a fallback to use instead:

let count = tryFind "visits" stats        // int option
let display = Option.defaultValue 0 count  // if empty, use 0

In plain words: "if there's a value, use it; if it's empty, use this default instead." Now display is always an honest int, with the missing case handled in the open rather than ignored.

Note

When you do want to handle each case yourself, reach for pattern matching. match box with | Some x -> ... | None -> ... lets you spell out both paths explicitly — and F# will warn you if you forget one, so the empty case can't slip away unnoticed.

Chaining lookups

Real code rarely stops at one lookup. To show a balance you find the user, then their account, then the balance — and any of the three might come up empty. With nulls, that means a null check before every step. With maybe-values, you chain the steps and let the boxes do the checking:

let balanceFor userId =
    findUser userId                  // User option
    |> Option.bind findAccount       // runs only if there's a user
    |> Option.bind findBalance       // runs only if there's an account

Each line hands the box to the next lookup only if the box is full. The moment one comes up empty, every step after it is skipped and None comes out the end. In plain words: if any step comes up empty, skip the rest.

Predict where the chain stops for user 7, who has no account yet. Then switch users to see the happy path and an early stop.

What makes this so calming is that the empty case stops being a landmine. With null, missing meant maybe a crash later. With a maybe-value, missing is just one of two clearly labelled states, and every step either handles it or politely steps around it.

There's one thing None can't tell you: which step came up empty, or why. When the caller needs the reason — "no such user" versus "account frozen" — swap the empty box for one that carries an error message. That's the success-or-failure railway, where a failure rides a parallel track to the end with its explanation attached.

Check yourself

Your four-step chain of lookups returns None, and support wants to know which lookup failed. What does the None tell you?

Key takeaways

  • `null` is the classic way to represent "no value" — but it hides the absence until your code touches it and crashes, often far from where the value went missing.
  • F#'s `Option` makes absence honest: a value is either `Some x` (it's here) or `None` (it's missing), right there in the type.
  • Because the type says a value *might* be missing, the compiler won't let you use it as if it were definitely there — yesterday's crash becomes today's compile error.
  • `Option.map` transforms the value inside the box if there is one, and `Option.defaultValue` supplies a fallback when the box is empty.
  • Chain lookups that might each come up empty, and the first empty one skips the rest: `None` comes out the end, with no null checks in between.

Keep going