F# pattern matching with optional list of tuples

Viewed 1202

I'm trying to use pattern matching for an optional list of tuples but I could not write an exhaustive matching expression despite trying everything I can think of.

I'm struggling to understand why the F# compiler is insisting that my patterns in the following examples are not exhaustive.

module Mapper.PatternMatchingOddity

type A = A of string
type B = B of string

type ProblemType = ProblemType of (A * B) list option

//Incomplete pattern matches on this expression. Some ([_;_]) may indicate a case...
let matchProblem = function
    |Some [(x:A,y:B)] -> []
    |Some ([_,_]) -> [] //rider says this rule will never be matched
    |None -> []

//same as before    
let matchProblem1 = function
    |Some [_,_] -> []
    |Some [] -> []
    //|Some _ -> []//this removes the warning but what is the case not covered by the previous two?
    |None -> []    

let matchProblem2 (input:ProblemType) =
    match input with //same as before
    |ProblemType (Some [(x:A,y:B)]) -> []
    |ProblemType None  -> []    

How do I write the exhaustive matching and what am I missing above? Can you give an example for an input that would be accepted as a valid parameter to these functions and slip through the patterns?

2 Answers

Great question! I think many people that start out with F# grapple with how lists, options and tuples interact. Let me start by saying: the compiler is correct. The short answer is: you are only matching over singleton lists. Let me try to explain that a little deeper.

Your type is ('a * 'b) list option, essentially. In your case, 'a and 'b are themselves a single-case discriminated using of a string. Let's simplify this a bit and see what happens if we look at each part of your type in isolation (you may already know this, but it may help to put it in context):

  1. First of all, your type is option. This has two values, None or Some 'a. To match over an option you can just do something like

    match o with 
    | Some value -> value 
    | None -> failwith "nothing"`
    
  2. Next, your type is a list. The items in a list are divided by semicolons ;. An empty list is [], a singleton list (one with a single item) is [x] and multiple items [x;y...]. To add something to the start of a list use ::. Lists are a special type of discriminated union and the syntax to match over them mimics the syntax of lists construction:

    match myList with
    | [] -> "empty"
    | [x] -> printfn "one item: %A" x
    | [x; y] -> printfn "two items: %A, %A" x y
    | x::rest -> printfn "more items, first one: %A" x
    
  3. Third, your list type is itself a tuple type. To deconstruct or match over a tuple type, you can use the comma ,, as with match (x, y) with 1, 2 -> "it's 1 and 2!" ....

  4. Combine all this, we must match over an option (outer) then list (middle) then tuple. Something like Some [] for an empty list and None for the absence of a list and Some [a, b] for a singleton list and Some (a,b)::rest for a list with one or more items.


Now that we have the theory out of the way, let's see if we can tackle your code. First let's have a look at the warning messages:

Incomplete pattern matches on this expression. Some ([_;_]) may indicate a case...

This is correct, the item in your code is separated by , denoting the tuple, and the message says Some [something; something] (underscore means "anything"), which is a list of two items. But it wouldn't help you much to add it, because the list can still be longer than 2.

rider says this rule will never be matched

Rider is correct (which calls the FSC compiler services underneath). The rule above that line is Some [(x:A,y:B)] (the :A and :B are not needed here), which matches any Some singleton array with a tuple. Some [_,_] does the same, except that it doesn't catch the values in a variable.

this removes the warning but what is the case not covered by the previous two?

It removes the warning because Some _ means Some with anything, as _ means just that: it is a placeholder for anything. In this case, it matches the empty list, the 2-item list, the 3-item list the n-item list (the only one your match is the 1-item list in that example).

Can you give an example for an input that would be accepted as a valid parameter

Yes. Valid input that you were not matching is Some [] (empty list), Some [A "a", B "x"; A "2", B "2"] (list of two items) etc.


Let's take your first example. You had this:

let matchProblem = function
    |Some [(x:A,y:B)] -> []  // matching a singleton list
    |Some ([_,_]) -> []   // matches a singleton list (will never match, see before)
    |None -> []  // matches None

Here's what you (probably) need:

let notAProblemAnymore = function
    // first match all the 'Some' matches:
    | Some [] -> "empty"  // an empty list
    | Some [x,y] -> "singleton"  // a list with one item that is a tuple
    | Some [_,a;_,b] -> "2-item list"  // a list with two tuples, ignoring the first half of each tuple
    | Some ((x,y)::rest) -> "multi-item list" 
           // a list with at least one item, and 'rest' as the 
           // remaining list, which can be empty (but won't, 
           // here it has at least three items because of the previous matches)
    | None -> "Not a list at all" // matching 'None' for absence of a list

To sum it up: you were matching over a list that had only one item and the compiler complained that you missed lists of other lengths (empty lists and lists that have more than one item).

Usually it is not necessary to use option with a list, because the empty list already means the absence of data. So whenever you find yourself writing the type option list consider whether just list would suffice. It will make the matching easier.

You are struggling because your example is too “example”.

Let’s convert your example to a more meaningful one: check the input, so that

  • If it is none then print “nothing”, otherwise:
  • If it has zero element then print “empty”
  • If it has only one element then print “ony one element: ...”
  • If it has two elements then print “we have two elements: ...”
  • If it has three elements then print “there are three elements: ...”
  • If it has more than three elements then print “oh man, the first element is ..., the second element is ..., the third element is ..., and N elements more”

Now you can see that your code only covers the first 3 cases. So the F# compiler was correct.

To rewrite the code:

let matchProblem (ProblemType input) =
    match input with
    | None -> printfn "nothing"
    | Some [] -> ...
    | Some [(x, y)] -> ...
    | Some [(x1, y1); (x2, y2)] -> ...
    | Some [(x1, y1); (x2, y2); (x3, y3)] -> ...
    | Some (x1, y1) :: (x2, y2) :: (x3, y3) :: rest -> // access rest.Length to print the number of more elements

Notice that I’m using pattern matching on the parameter ProblemType input so that I can extract the input in a convenient way. This makes the later patterns simpler.

Personally, when I learned F#, I didn’t understand many features/syntax until I used them in production code.

Related