haskelltype-systemsgadtdata-kindsphantom-types

The limit set of types with new data like `Tree a`


Exploring and studing type system in Haskell I've found some problems.

1) Let's consider polymorphic type as Binary Tree:

 data Tree a = Leaf a | Branch (Tree a) (Tree a) deriving Show

And, for example, I want to limit my considerations only with Tree Int, Tree Bool and Tree Char. Of course, I can make a such new type:

data TreeIWant = T1 (Tree Int) | T2 (Tree Bool) | T3 (Tree Char) deriving Show

But could it possible to make new restricted type (for homogeneous trees) in more elegant (and without new tags like T1,T2,T3) way (perhaps with some advanced type extensions)?

2) Second question is about trees with heterogeneous values. I can do them with usual Haskell, i.e. I can do the new helping type, contained tagged heterogeneous values:

data HeteroValues = H1 Int | H2 Bool | H3 Char deriving Show

and then make tree with values of this type:

type TreeH = Tree HeteroValues

But could it possible to make new type (for heterogeneous trees) in more elegant (and without new tags like H1,H2,H3) way (perhaps with some advanced type extensions)? I know about heterogeneous list, perhaps it is the same question?


Solution

  • For question #2, it's easy to construct a "restricted" heterogeneous type without explicit tags using a GADT and a type class:

    {-# LANGUAGE GADTs #-}
    data Thing where
      T :: THING a => a -> Thing
    class THING a
    

    Now, declare THING instances for the the things you want to allow:

    instance THING Int
    instance THING Bool
    instance THING Char
    

    and you can create Things and lists (or trees) of Things:

    > t1 = T 'a'       -- Char is okay
    > t2 = T "hello"   -- but String is not
    ... type error ...
    > tl = [T (42 :: Int), T True, T 'x']
    > tt = Branch (Leaf (T 'x')) (Leaf (T False))
    >
    

    In terms of the type names in your question, you have:

    type HeteroValues = Thing
    type TreeH = Tree Thing
    

    You can use the same type class with a new GADT for question #1:

    data ThingTree where
      TT :: THING a => Tree a -> ThingTree
    

    and you have:

    type TreeIWant = ThingTree
    

    and you can do:

    > tt1 = TT $ Branch (Leaf 'x') (Leaf 'y')
    > tt2 = TT $ Branch (Leaf 'x') (Leaf False)
    ... type error ...
    >
    

    That's all well and good, until you try to use any of the values you've constructed. For example, if you wanted to write a function to extract a Bool from a possibly boolish Thing:

    maybeBool :: Thing -> Maybe Bool
    maybeBool (T x) = ...
    

    you'd find yourself stuck here. Without a "tag" of some kind, there's no way of determining if x is a Bool, Int, or Char.

    Actually, though, you do have an implicit tag available, namely the THING type class dictionary for x. So, you can write:

    maybeBool :: Thing -> Maybe Bool
    maybeBool (T x) = maybeBool' x
    

    and then implement maybeBool' in your type class:

    class THING a where
      maybeBool' :: a -> Maybe Bool
    instance THING Int where
      maybeBool' _ = Nothing
    instance THING Bool where
      maybeBool' = Just
    instance THING Char where
      maybeBool' _ = Nothing
    

    and you're golden!

    Of course, if you'd used explicit tags:

    data Thing = T_Int Int | T_Bool Bool | T_Char Char
    

    then you could skip the type class and write:

    maybeBool :: Thing -> Maybe Bool
    maybeBool (T_Bool x) = Just x
    maybeBool _ = Nothing
    

    In the end, it turns out that the best Haskell representation of an algebraic sum of three types is just an algebraic sum of three types:

    data Thing = T_Int Int | T_Bool Bool | T_Char Char
    

    Trying to avoid the need for explicit tags will probably lead to a lot of inelegant boilerplate elsewhere.

    Update: As @DanielWagner pointed out in a comment, you can use Data.Typeable in place of this boilerplate (effectively, have GHC generate a lot of boilerplate for you), so you can write:

    import Data.Typeable
    
    data Thing where
      T :: THING a => a -> Thing
    class Typeable a => THING a
    
    instance THING Int
    instance THING Bool
    instance THING Char
    
    maybeBool :: Thing -> Maybe Bool
    maybeBool = cast
    

    This perhaps seems "elegant" at first, but if you try this approach in real code, I think you'll regret losing the ability to pattern match on Thing constructors at usage sites (and so having to substitute chains of casts and/or comparisons of TypeReps).