From what I have seen, pattern matching is overused in F#. Especially when dealing with options. It’s not hard to spot the following kind of pattern in F# codebases:

match x with
| Some a -> Some <| f a
| None -> None

Here is what’s wrong with this kind of code:

  1. The intent of the code is not immediately obvious. i.e. low level of abstraction.
  2. Verbosity.
  3. Pattern matching does not compose.

We can alleviate all of the above drawbacks by making use of higher order functions that abstract over common patterns needed for dealing with options.

For example, with higher order function - map, the above code can be written more concisely as:

x |> map f

Tony Morris once blogged an Option cheat sheet for Scala. What follows is a F# translation of the same, with a couple of additional functions.

Several of the following functions are already a part of F#’s Option module. You should add the rest to your repertoire by extending the Option module. Alternatively you can use FSharpx which provides most of these (and many other) functions.

// map : ('a -> 'b) -> 'a option -> 'b option
let map f =
  function
  | Some a -> Some <| f a
  | None -> None

// filter : ('a -> bool) -> 'a option -> 'a option
let filter cond =
  function
  | Some a as s when cond a -> s
  | _ -> None

// iter : ('a -> unit) -> 'a option -> unit
let iter f =
  function
  | Some a -> f a
  | None -> ()

// fold : ('a -> 'b) -> 'b -> 'a option -> 'b
let fold f g =
  function
  | Some a -> f a
  | None -> g

// bind : ('a -> 'b option) -> 'a option -> 'b option
let bind f =
  function
  | Some a -> f a
  | None -> None

// flatten : 'a option option -> 'a option
let flatten =
  function
  | Some a -> a
  | None -> None

// getOrElse : 'a -> 'a option -> 'a
let getOrElse d = 
  function
  | Some a -> a
  | None -> d

// orElse : 'a option -> 'a option -> 'a option
let orElse b a =
  match a with
  | Some _ -> a
  | None -> b

// isSome : 'a option -> bool
let isSome =
  function
  | Some _ -> true
  | None -> false

// isNone : 'a option -> bool
let isNone =
  function
  | Some _ -> false
  | None -> true

// forall : ('a -> bool) -> 'a option -> bool
let forall cond =
  function
  | Some a -> cond a
  | None -> true

// exists : ('a -> bool) -> 'a option -> bool
let exists cond =
  function 
  | Some a -> cond a
  | None -> false

// toList : 'a option -> 'a list
let toList =
  function
  | Some a -> a :: []
  | None -> [] 

// lift2 : ('a -> 'b -> 'c) -> 'a option -> 'b option -> 'c option
let lift2 f x y =
  match x with 
  | None -> None
  | Some x' -> 
      match y with
      | None -> None
      | Some y' -> Some <| f x' y'
   
// lift3 : ('a -> 'b -> 'c -> 'd) -> 'a option -> 'b option -> 
// 'c option -> 'd option
let lift3 f x y z = 
  match x with 
  | None -> None
  | Some x' -> 
     match y with
     | None -> None
     | Some y' -> 
         match z with
         | None -> None
         | Some z' -> Some <| f x' y' z'

Here is some code I wrote not too long ago. (The original was in Scala. Here translated to F#.) The task is thus: You receive a HTTP POST request. The required parameter values come in the URI, and you receive one file as an attachment. If all of these arguments are present and well-formed, you have to compute something, and return it back. This is how the code for this looks with option higher order functions:

let customerCode = 
  headers |> Map.tryFind "customer_code"
  
let sheetNr = 
  headers |> Map.tryFind "sheet_nr" |> Option.bind Int32.parse

let file = 
  headers 
  |> Map.tryFind "attached_file" 
  |> Option.bind (flip Map.tryFind attachments)

let result = 
  Option.lift3 parseFile file customerCode sheetNr

respond <| 
  match result with
  | Some (Choice1Of2 parsedValue) -> (200, parsedValue)
  | Some (Choice2Of2 errors) -> (400, toJson errors)
  | None -> (400, "Bad params")

In the above code, headers and attachments are assumed to have types Map<string, string> and Map<string, File> respectively. Int32.parse is a safe string to integer conversion function from FSharpx with type string -> int option. parseFile has type File -> string -> int -> Choice<string, string list>. lift3 lifts it to the type File option -> string option -> int option -> Choice<string, string list> option. flip is yet another function from FSharpx, used for flipping function arguments i.e. it turns 'a -> 'b -> 'c into 'b -> 'a -> 'c. respond is assumed to be a function for sending back response, typed (int, string) -> unit.

Now, my example above also happens to use pattern matching. Yes, there are justifiable use cases for pattern matching on option. This is when you should use it:

  1. When each case requires elaborate dealing with. Higher order function equivalent of that would be fold, but it looks uglier if the function being passed is anything more than a one-liner.
  2. When you are dealing with object graphs more than one level deep. e.g. Matching over Choice<'a, 'b> option.
  3. When matching over multiple options. e.g. int option * int option.

Everywhere else, higher order functions make for more readable code.

(Special thanks to Mauricio Scheffer for reviewing the draft and suggesting improvements.)