From what I have seen, pattern matching is overused in F#, especially when dealing with options. It is not hard to spot code like this in F# codebases:
match x with| Some a -> Some <| f a| None -> NoneHere is what is wrong with this style of code:
- The intent is not immediately obvious; the abstraction level is too low.
- It is verbose.
- Pattern matching does not compose.
We can alleviate all of these drawbacks by using higher-order functions that abstract over common option patterns.
For example, with the higher-order function map, the above code can be written more concisely as:
x |> map fTony Morris once blogged an Option cheat sheet for Scala. What follows is an F# translation of the same idea, with a couple of additional functions.
Several of the following functions are already 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 functions, along with many others.
// map : ('a -> 'b) -> 'a option -> 'b optionlet map f = function | Some a -> Some <| f a | None -> None
// filter : ('a -> bool) -> 'a option -> 'a optionlet filter cond = function | Some a as s when cond a -> s | _ -> None
// iter : ('a -> unit) -> 'a option -> unitlet iter f = function | Some a -> f a | None -> ()
// fold : ('a -> 'b) -> 'b -> 'a option -> 'blet fold f g = function | Some a -> f a | None -> g
// bind : ('a -> 'b option) -> 'a option -> 'b optionlet bind f = function | Some a -> f a | None -> None
// flatten : 'a option option -> 'a optionlet flatten = function | Some a -> a | None -> None
// getOrElse : 'a -> 'a option -> 'alet getOrElse d = function | Some a -> a | None -> d
// orElse : 'a option -> 'a option -> 'a optionlet orElse b a = match a with | Some _ -> a | None -> b
// isSome : 'a option -> boollet isSome = function | Some _ -> true | None -> false
// isNone : 'a option -> boollet isNone = function | Some _ -> false | None -> true
// forall : ('a -> bool) -> 'a option -> boollet forall cond = function | Some a -> cond a | None -> true
// exists : ('a -> bool) -> 'a option -> boollet exists cond = function | Some a -> cond a | None -> false
// toList : 'a option -> 'a listlet toList = function | Some a -> a :: [] | None -> []
// lift2 : ('a -> 'b -> 'c) -> 'a option -> 'b option -> 'c optionlet 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 optionlet 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 it is translated to F#. The task is this: you receive an 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 compute something and return it.
This is what the code looks like with higher-order functions over option:
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 the types Map<string, string> and Map<string, File> respectively. Int32.parse is a safe string-to-integer conversion function from FSharpx with the type string -> int option. parseFile has the 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 another function from FSharpx, used for flipping function arguments; it turns 'a -> 'b -> 'c into 'b -> 'a -> 'c. respond is assumed to be a function for sending back the response, with the type (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:
- When each case requires elaborate handling. The higher-order-function equivalent would be
fold, but it looks uglier if the function being passed is anything more than a one-liner. - When you are dealing with object graphs more than one level deep, for example matching over
Choice<'a, 'b> option. - When matching over multiple options, for example
int option * int option.
Everywhere else, higher-order functions usually make for more readable code.
Special thanks to Mauricio Scheffer for reviewing the draft and suggesting improvements.