Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can you pattern match constructors on a type class constrained parameter?

See code example below. It won't compile. I had thought that maybe it's because it has to have a single type for the first parameter in the test function. But that doesn't make sense because if I don't pattern match on it so it will compile, I can call it with both MyObj11 5 and MyObj21 5 which are two different types.

So what is it that restricts so you can't pattern match on constructors with a type class constrained parameter? Or is there some mechanism by which you can?

class SomeClass a where toString :: a -> String

instance SomeClass MyType1 where toString v = "MyType1"
instance SomeClass MyType2 where toString v = "MyType2"

data MyType1 = MyObj11 Int | MyObj12 Int Int 
data MyType2 = MyObj21 Int | MyObj22 Int Int 

test :: SomeClass a => a -> String
test (MyObj11 x) = "11"
test (MyObj12 x y) = "12" -- Error here if remove 3rd line: rigid type bound error
test (MyObj22 x y) = "22" -- Error here about not match MyType1.
like image 221
mentics Avatar asked Apr 22 '11 17:04

mentics


1 Answers

what is it that restricts so you can't pattern match on constructors with a type class constrained parameter?

When you pattern match on an explicit constructor, you commit to a specific data type representation. This data type is not shared among all instances of the class, and so it is simply not possible to write a function that works for all instances in this way.

Instead, you need to associate the different behaviors your want with each instance, like so:

class C a where 
    toString   :: a -> String
    draw       :: a -> String

instance C MyType1 where
    toString v = "MyType1"

    draw (MyObj11 x)   = "11"  
    draw (MyObj12 x y) = "12"

instance C MyType2 where
    toString v = "MyType2"

    draw (MyObj22 x y) = "22"

data MyType1 = MyObj11 Int | MyObj12 Int Int 
data MyType2 = MyObj21 Int | MyObj22 Int Int 

test :: C a => a -> String
test x = draw x

The branches of your original test function are now distributed amongst the instances.

Some alternative tricks involve using class-associated data types (where you prove to the compiler that a data type is shared amongst all instances), or view patterns (which let you generalize pattern matching).


View patterns

We can use view patterns to clean up the connection between pattern matching and type class instances, a little, allowing us to approximate pattern matching across instances by pattern matching on a shared type.

Here's an example, where we write one function, with two cases, that lets us pattern match against anything in the class.

{-# LANGUAGE ViewPatterns #-}

class C a where 
    view       :: a -> View

data View = One Int
          | Two Int Int

data MyType1 = MyObj11 Int | MyObj12 Int Int 

instance C MyType1 where
    view (MyObj11 n) = One n
    view (MyObj12 n m) = Two n m

data MyType2 = MyObj21 Int | MyObj22 Int Int 

instance C MyType2 where
    view (MyObj21 n)   = One n
    view (MyObj22 n m) = Two n m

test :: C a => a -> String
test (view -> One n)   = "One " ++ show n
test (view -> Two n m) = "Two " ++ show n ++ show m

Note how the -> syntax lets us call back to the right view function in each instance, looking up a custom data type encoding per-type, in order to pattern match on it.

The design challenge is to come up with a view type that captures all the behavior variants you're interested in.

In your original question, you wanted every constructor to have a different behavior, so there's actually no reason to use a view type (dispatching directly to that behavior in each instance already works well enough).

like image 86
Don Stewart Avatar answered Sep 23 '22 10:09

Don Stewart