F# list pattern matching limitations or just bad writing?

Viewed 59

Given some CreditScoreInput:

type CreditScoreInput = { id: string; score: string; years: int }

let input = [
    { id = "CUSTOMER001"; score = "Medium"; years = 1 }
    { id = "CUSTOMER001"; score = "Medium"; years = 1 }
    { id = "CUSTOMER002"; score = "Medium"; years = 10 }
    { id = "CUSTOMER003"; score = "Bad"; years = 0 }
    { id = "CUSTOMER003"; score = "Bad"; years = 0 }
    { id = "CUSTOMER003"; score = "Bad"; years = 0 }
    { id = "CUSTOMER004"; score = "Good"; years = 0 }
    { id = "CUSTOMER005"; score = "Good"; years = 10 }
]

My function validateDuplicates is out looking for duplicates:

let validate list =
    match list with
    | [] -> failwith "No customers supplied!"
    | _ -> list

let validateDuplicates (group:string * list<CreditScoreInput>) =
    match group with
    | (id, g) when g.Length = 1 -> printf $"No duplicates for {id} is OK.\n"
    | (id, [input1; input2]) -> printf $"Two duplicates for {id} is OK.\n"
    | (id, g) when g.Length > 2 -> printf $"More than two duplicates for {id} is NOT OK.\n"

    true

input
|> validate
|> List.groupBy (fun i -> i.id)
|> List.forall (fun i -> i |> validateDuplicates)
|> ignore

Inside of validateDuplicates I notice a little squiggly under group, leading to the warning:

Incomplete pattern matches on this expression. For example, the value (_,[_;_;_]) may indicate a case not covered by the pattern(s). However, a pattern rule with a when clause might successfully match this value.

Is there a way I can play nice with the compiler to avoid this warning?

Update

I am not sure whether I should do this here but here are my changes based on the excellent guidance:

let validateDuplicates (group:string * list<CreditScoreInput>) =
    match group with
    | (id, [_]) -> printf $"No duplicates for {id} is OK.\n"
    | (id, [_; _]) -> printf $"Two duplicates for {id} is OK.\n"
    | (id, g) -> printf $"More than two duplicates for {id} is NOT OK.\n"

input
|> validate
|> List.groupBy (fun i -> i.id)
|> List.iter (fun i -> i |> validateDuplicates)
1 Answers

Just get rid of the last when clause, because you know it will always be true:

    match group with
    | (id, g) when g.Length = 1 -> printf $"No duplicates for {id} is OK.\n"
    | (id, [input1; input2]) -> printf $"Two duplicates for {id} is OK.\n"
    | (id, g) -> printf $"More than two duplicates for {id} is NOT OK.\n"

Proof:

  • g.Length can never be negative or 0
  • If g.Length is 1 then it will match the first case
  • If g.Length is 2 then it will match the second case
  • Therefore, g.Length will always be > 2 if control reaches the third case.

Here's how I would suggest you write this code instead:

let validateDuplicates (id, g : List<_>) =
    match g.Length with
    | 0 -> failwith "Unexpected"
    | 1 -> printf $"No duplicates for {id} is OK.\n"
    | 2 -> printf $"Two duplicates for {id} is OK.\n"
    | _ -> printf $"More than two duplicates for {id} is NOT OK.\n"

input
|> validate
|> List.groupBy (fun i -> i.id)
|> List.iter validateDuplicates

The changes I've made are:

  • Use List.iter instead of piping List.forall into ignore.
  • Eliminate unneeded lambda in invocation of validateDuplicates.
  • Use pattern matching to deconstruct the input to validateDuplicates
  • Match directly on g.Length instead of using when clauses.
  • Defensive programming: explicitly check for an empty list to make your intention clear.

You might also want to consider making your validation functions pure (i.e. no side-effects) via F#'s Result type.

Related