15 April 2026

Options in F#

One type of discriminated union that is commonly used in F# is the Option type. The Option type is used to represent a value that may be present or absent. It is defined as follows:

type Option<'T> =
    | Some of 'T
    | None

The Option type is generic; we can use with any value type but it’s more like a wrapper around a value, rather than a value itself. Let’s see how it works:

> let aNumber = 5;;                       // regular int
val aNumber: int = 5

> let maybeNumber = Some 10;;             // optional int that is present
val maybeNumber: int option = Some 10

> let missingNumber : int option = None;; // optional int that is absent
val missingNumber: int option = None

It’s important to note that maybeNumber and missingNumber are of type int option, which is different from aNumber, which is of type int. This means you can’t do this:

> let sum = aNumber + maybeNumber;;       // error: type mismatch

Instead, we have to pattern match on the Option type to access the value inside it:1

> let sum =
-     match maybeNumber with
-     | Some number -> $"sum = {aNumber + number}"
-     | None -> "Cannot perform operation";;
val sum: string = "sum = 15"

To help work with Option types, F# provides some useful functions in the Option module, two of which include Option.map and Option.bind. To see how Option.map works, let first examine it’s type signature:

Option.map : ('a -> 'b) -> 'a option -> 'b option

What this means is that Option.map takes:

  1. a function that transforms a value of type 'a -> 'b

  2. an Option of type 'a option

and applies that function on 'a option to return an Option of type 'b option. For example, let’s say we have a function that doubles an integer:

> let double x = x * 2;;
val double: x: int -> int

If we try to pass maybeNumber, which is of type int option, directly to double, which is expecting an int, we get a type error:

  maybeNumber |> double;;
  ---------------^^^^^^

error FS0001: Type mismatch. Expecting a
    'int option -> 'a'    
but given a
    'int -> int'    
The type 'int' does not match the type 'int option'

We can instead use Option.map to apply the function double on maybeNumber:

> maybeNumber |> Option.map double;;
val it: int option = Some 20

Option.map can also gracefully handle the case when the Option is None:

> missingNumber |> Option.map double;;
val it: int option = None

In the example above, our function double took an int and returned an int, but Option.map transformed it to work with Option types, taking an int option and returning an int option. The way I think about it is that Option.map “unwraps” maybeNumber to a normal int, applies the function, and then “rewraps” the return value with the Option type.

But what if we have a function that returns an Option type? For example, let’s say we have the following function to check whether the divisor is zero:

> let tenDividedBy x =
-     match x with    
-     | 0 -> None     
-     | _ -> Some (10 / x);;
val tenDividedBy: x: int -> int option

Like before, with our double function, we can’t directly pass maybeNumber, which is of type int option, to tenDividedBy which is expecting an int:

> maybeNumber |> tenDividedBy;;

  maybeNumber |> tenDividedBy;;
  ---------------^^^^^^^^^^^^

error FS0001: Type mismatch. Expecting a
    'int option -> 'a'    
but given a
    'int -> int option'    
The type 'int' does not match the type 'int option'

However, we can’t use Option.map here either:

> maybeNumber |> Option.map tenDividedBy;;
val it: int option option = Some (Some 1)

as it returns a nested Option type of int option option, which is not what we want. (Using my thinking from above, Option.map “unwrapped” maybeNumber to an int, applied the function which returned an Option type, and then “wrapped” that in another Option.)

Instead, we can use Option.bind, which has the following type signature:

Option.bind : ('a -> 'b option) -> 'a option -> 'b option

The only difference between Option.map and Option.bind is that the function passed to Option.bind must return an Option type. This allows Option.bind to “unwrap” the nested Option type that we got from using Option.map (i.e. Option.bind doesn’t “rewrap” what was returned from the transform function). If I had to guess, bind refers binding the final type to what was returned from the function, but don’t quote me on that! So now we can do this:

> maybeNumber |> Option.bind tenDividedBy;;
val it: int option = Some 1

> missingNumber |> Option.bind tenDividedBy;;
val it: int option = None

> (Some 0) |> Option.bind tenDividedBy;;
val it: int option = None

  1. You can also use the Option.get function to directly extract the value from an Option (i.e. let sum = aNumber + Option.get maybeNumber), but this will throw an exception if the Option is None, so it’s generally safer to use pattern matching or the functions in the Option module.↩︎