haskellgadt

Conditional typeclass in GADT


I have a GADT like

data GADT where
  Data :: a -> GADT

and I want to implement a Show instance depending on where Show a exists. Intuitively, something like

instance Show GADT where
  show (Data a) = if isShow a then show a else "UNSHOWABLE"

Is this actually expressible in Haskell? I do use Typeable a in my actual GADT, so I could use that if it helps.


Solution

  • You said "depending on where Show a exists", but I assume this means "depending on whether Show a exists". If so, then the answer is no, Haskell does not let you write code that does one thing if a type has a particular type class instance and another thing if it doesn't.

    You can, however, define an overlappable, orphan Show instance to serve as a default, like so:

    instance {-# OVERLAPPABLE #-} Show a where
      show _ = "UNSHOWABLE"
    

    If you define your GADT with a Show constraint:

    data GADT where
      Data :: (Show a) => a -> GADT
    

    then you can define its Show instance like so:

    instance Show GADT where
      show (Data a) = show a
    

    and it "works", after a fashion:

    λ> show (Data 'a')
    "'a'"
    λ> show (Data id)
    "UNSHOWABLE"
    

    But, there's no legitimate way to isolate this behavior to your GADT. The default Show instance will be available to non-GADT types as well:

    λ> show 'a'
    "'a'"
    λ> show id
    "UNSHOWABLE"
    

    Many people think they ought to be able to write something like the following (maybe with overlapping instances):

    class MyShow a where
      myShow :: a -> String
    
    instance (Show a) => MyShow a where
      myShow = show
    instance MyShow a where
      myShow _ = "UNSHOWABLE"
    

    But this doesn't work. GHC doesn't decide whether an instance applies or not based on its constraints, so it interprets these as two conflicting definitions for the same set of types (i.e., all types a) and refuses to compile this code, even with a liberal sprinkling of {-# OVERLAPS #-} pragmas.

    If you are willing to explicitly enumerate types (either the types that should be "UNSHOWABLE" or the types that are Showable), then you can construct MyShow class with valid overlapping instances:

    -- Example enumerating the showable types (Int and Char)
    class MyShow a where myShow :: a -> String
    instance {-# OVERLAPPABLE #-} MyShow a where myShow _ = "UNSHOWABLE"
    instance MyShow Int where myShow = show
    instance MyShow Char where myShow = show
    
    -- Example enumerating the unshowable types (a -> b)
    class MyShow' a where myShow' :: a -> String
    instance {-# OVERLAPPABLE #-} (Show a) => MyShow' a where myShow' = show
    instance MyShow' (a -> b) where myShow' _ = "UNSHOWABLE"
    

    You may think this is a totally unacceptable approach, but I encourage you to try it in a real program. You may discover that there simply aren't that many types that need to be enumerated, and the boilerplate just isn't all that burdensome.