Sunday, March 20, 2011

Can you nest a "double cons" pattern match?

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.

From stackoverflow
  • 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.

    Pete Montgomery : 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.
  • 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
    

0 comments:

Post a Comment