3
votes

Apologies in advance for the long example, I couldn't figure out a shorter one.

Let's define a type class Box, that does nothing but contain another type, the Content.

{-# LANGUAGE GADTs                     #-}
{-# LANGUAGE TypeFamilies              #-}

class Box t where
    type Content t

data IntBox = IntBox
data StringBox = StringBox

Let's write a few instances:

instance Box IntBox where
    type Content IntBox = Int

instance Box StringBox where
    type Content StringBox = String

data JointBox a b = JointBox a b

instance (Box a, Box b) => Box (JointBox a b) where
    type Content (JointBox a b) = Either (Content a) (Content b)

That all compiles and works so far. Enter GADTs. I want an algebraic data type that is constructed out of a box and its contents. The constructor decides the box type completely.

data ABox t where
    AnIntBox :: IntBox -> ABox IntBox
    AStringBox :: StringBox -> ABox StringBox
    AnIntOrStringBox :: JointBox IntBox StringBox -> ABox (JointBox IntBox StringBox)

Now, in my mind, this would mean that by pattern matching on the constructor of ABox, the type of the box and its contents should be determined. But this doesn't seem to be the case:

frobABox :: (Content t) ->  ABox t              -> IO ()
frobABox     int           (AnIntBox _)         =  print $ int + 3
frobABox     string        (AStringBox _)       =  putStrLn $ reverse string
frobABox    (Left int)     (AnIntOrStringBox _) =  print $ int + 6
frobABox    (Right string) (AnIntOrStringBox _) =  putStrLn $ reverse string

This throws a lot of errors, amongst which these two seem the most important to me:

GADTAndTypeClassBug.hs:29:14:
    Couldn't match expected type ‘Content t’
                with actual type ‘Either t0 t1’
    The type variables ‘t0’, ‘t1’ are ambiguous
    Relevant bindings include
      frobABox :: Content t -> ABox t -> IO ()
        (bound at GADTAndTypeClassBug.hs:27:1)
    In the pattern: Left int
    In an equation for ‘frobABox’:
        frobABox (Left int) (AnIntOrStringBox _) = print $ int + 6

GADTAndTypeClassBug.hs:30:14:
    Couldn't match expected type ‘Content t’
                with actual type ‘Either t2 t3’
    The type variables ‘t2’, ‘t3’ are ambiguous
    Relevant bindings include
      frobABox :: Content t -> ABox t -> IO ()
        (bound at GADTAndTypeClassBug.hs:27:1)
    In the pattern: Right string
    In an equation for ‘frobABox’:
        frobABox (Right string) (AnIntOrStringBox _)
          = putStrLn $ reverse string

GADTAndTypeClassBug.hs:30:71:
    Couldn't match expected type ‘[Char]’ with actual type ‘t3’
      ‘t3’ is untouchable
        inside the constraints (t ~ JointBox IntBox StringBox)
        bound by a pattern with constructor
                   AnIntOrStringBox :: JointBox IntBox StringBox
                                       -> ABox (JointBox IntBox StringBox),
                 in an equation for ‘frobABox’
        at GADTAndTypeClassBug.hs:30:29-46
    Relevant bindings include
      string :: t3 (bound at GADTAndTypeClassBug.hs:30:20)
    In the first argument of ‘reverse’, namely ‘string’
    In the second argument of ‘($)’, namely ‘reverse string’

An easier example without the type families works:

data UnitOrEither t where
    AUnit :: () -> UnitOrEither ()
    AnEither :: Either String Int -> UnitOrEither (Either String Int)

frob :: UnitOrEither t -> IO ()
frob   (AUnit _)       =  print ()
frob   (AnEither _)    =  print "Either"

So what's the problem?

1
You don't need a type class just to get a type family. You could write type family Content t; type instance (JointBox a b) = Either (Content a) (Content b) (and similarly for IntBox/StringBox). This is less code, and doesn't introduce a type-level name you don't care about.Daniel Wagner
@DanielWagner, right, I just reduced the actual example I was working with.Turion

1 Answers

7
votes

Refinement for GADT pattern matches works left-to-right. So a type refinement resulting from matching on the second argument of frobABox will not apply to the first.

You can make the code compile by flipping the arguments of frobABox:

frobABox' :: ABox t              -> Content t -> IO ()
frobABox' (AnIntBox _)          int           =  print $ int + 3
frobABox' (AStringBox _)        string        =  putStrLn $ reverse string
frobABox' (AnIntOrStringBox _) (Left int)     =  print $ int + 6
frobABox' (AnIntOrStringBox _) (Right string) =  putStrLn $ reverse string

frobABox :: (Content t) ->  ABox t              -> IO ()
frobABox = flip frobABox'