2

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?

Vladimir
  • 541
  • 2
  • 8
  • 2
    The answer about *hetrogenous lists* can be applied to trees (without much) modification, except for using `fmap` over `map`, etc. The principle is demonstrated on a list, but you can use it on all sorts of "functor-like" things. – Willem Van Onsem Jun 28 '19 at 11:17
  • 4
    There are several ways to do roughly what you asked for. I wouldn't say any of them is really better than the explicitly-tagged alternative-type, but perhaps if you explain the application you need this for we can give some hints. – leftaroundabout Jun 28 '19 at 11:50

1 Answers1

2

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).

K. A. Buhr
  • 45,621
  • 3
  • 45
  • 71