views:

215

answers:

2

I want to strengthen a pattern to match only numbers which pass an additional validation function.

let (|IsValid|_|) n = ...

let (|Nil|One|Two|) (l : int list) =
    match l with 
    | a :: b :: t -> Two(a + b)
    | a :: t      -> One(a)
    | _           -> Nil

The 'One' case is easy:

    | IsValid(a) :: t -> One(a)

The 'Two' case isn't obvious to me. It needs to validate the sum of the numbers. Can I do this without using a when-guard?

...

Edit: I could use a when-guard (with a bool-returning isValid function) like this:

    | a :: b :: t when isValid a + b -> Two(a + b)

This is less elegant than just matching a pattern; worse, a + b is applied twice.

Also note this is a simplified version of my actual code (I'm not trying to simply match against different lengths of list for example) - the question is about nested matching over the double cons pattern.

A: 

When you do:

| a :: b :: t -> ...

You aren't necessarily matching two elements in a list. It's better to use [] instead of t to match two elements exactly --t can be a list of more elements.

 | a :: b :: [] -> Two (a+b)

This will ensure that you are matching two and only two elements --error checking for free! I suggest doing this even though you expect the function to only accept a list of 0, 1, or 2 elements. So,

EDIT:

let (|MatchTwo|_|) = function
    | a :: b :: t -> Some(a + b :: t)
    | _ -> None
let (|Nil|One|Two|) (l : int list) = match l with 
    | MatchTwo(IsValid(a) :: t) -> Two(a)
    | IsValid(a) :: t  -> One(a)
    | _ -> Nil

Yeah, use when. This is a mess. Pattern matching is just that, applying functions within the match really doesn't make sense. But take into account what I've mentioned prior. Based on your example:

match l with
| a :: b :: t when isValid (a+b) -> Two (a+b)
| a :: t when isValid (a) -> One a
| _ -> Nil

The second pattern will match lists of length longer then one if isValid is false on the first pattern --be warned. Be as specific in your patterns as possible, if you mean to match one element, do it.

If whatever operation you use to combine a and b (in this case +) is computationally expensive, then you will have to drop the when and use a let statement before testing the isValid and returning the variant type.

nlucaroni
Thanks, but I've maybe simplified my real code badly. I actually don't want to match a list with only 0, 1 or 2 elements. But this is still completely irrelevant to the question!
Pete Montgomery
Sorry, I didn't mean to sound so terse in my last comment! Thanks for taking your time to help me.
Pete Montgomery
A: 

My solution: Add a "helper" recogniser with a return value designed to be used in the parent pattern:

let (|MatchTwo|_|) = function
    | a :: b :: t -> Some(a + b :: t)
    | _ -> None

Use it like so:

let (|Nil|One|Two|) (l : int list) =
    match l with 
    | MatchTwo(IsValid(a) :: t) -> Two(a)
    |          IsValid(a) :: t  -> One(a)
    | _                         -> Nil
Pete Montgomery