Search code examples
haskellpattern-matchinggeneric-programminggadt

How to express pattern matching for independent combinations of data values?


data Foo = Bar1
         | Bar2 Foo Foo
         | Bar3 Foo
         | Bar4 Foo Foo Foo

Now, assume someone built a Foo tree and I want to check if the arguments of a Foo value are valid. The rules on the constructor arguments are:

  • Bar2 Bar1 Foo
  • Bar3 (Bar2|Bar3|Bar4)
  • Bar4 Bar1 Bar1 (Bar1|Bar4)

I know the constructor of the value and only want to check the immediate arguments, nothing recursive. Like this:

bar2 Bar1 Bar1      = True
bar2 Bar1 (Bar2 {}) = True
bar2 Bar1 (Bar3 _)  = True
bar2 Bar1 (Bar4 {}) = True
bar2 _ _ = False

And e.g. similarly for Bar4:

bar4 Bar1 Bar1 Bar1      = True
bar4 Bar1 Bar1 (Bar4 {}) = True
bar4 _ _ _ = False

How can I express these conditions most concisely? Listing all combinations is in some cases a bit too much. And to my knowledge an "OR"-syntax for pattern matching doesn't exist.

UPDATE

I adapted Daniel's solution and have come to this:

data Foo = Bar1
         | Bar2 Foo Foo
         | Bar3 Foo
         | Bar4 Foo Foo Foo
         deriving (Data, Typeable)

bar2 a b = a `isOf` [Bar1] && b `isOf` [Bar1,Bar2{},Bar3{},Bar4{}]
bar4 a b c = [a,b] `areOf` [Bar1] && c `isOf` [Bar1,Bar4{}]

isOf l r = toConstr l `elem` map toConstr r
areOf l r = all (`isOf` r) l

What I like about this is that I don't have to change my data type, except adding the deriving clause, and that it's readable. The disadvantage of course is that these are dynamic checks. In my case this is fine though as it's just for assert-like checks to discover programming errors.


Solution

  • There is a fine static-checking solution posted; here's a suggestion for a dynamic-checking solution. The key idea is to shy away from pattern-matching and use all the tools we have for writing compact code instead. There's a couple choices for doing this; I'll discuss two. The first is to write isBarX functions for each constructor:

    isBar1 (Bar1 {}) = True
    isBar1 _ = False
    
    -- ...
    
    isBar4 (Bar4 {}) = True
    isBar4 _ = False
    
    valid (Bar1)       = True
    valid (Bar2 a b)   = isBar1 a
    valid (Bar3 a)     = not (isBar1 a)
    valid (Bar4 a b c) = isBar1 a && isBar1 b && (isBar1 c || isBar4 c)
    

    Another option is to write a function that returns some data telling which constructor was used -- say, a custom type like data Constructor = CBar1 | CBar2 | CBar3 | CBar4, or, as I'll do below, something a little hackier, like Int.

    constr (Bar1 {}) = 1
    constr (Bar2 {}) = 2
    constr (Bar3 {}) = 3
    constr (Bar4 {}) = 4
    
    valid (Bar1)       = True
    valid (Bar2 a b)   = constr a == 1
    valid (Bar3 a)     = constr a /= 1
    valid (Bar4 a b c) = constr a == 1 && constr b == 1 && constr c `elem` [1,4]