Haskell type synonym declaration with constraint possible?

Use data and parameters, not existentials to get contexts

I think if you wanted

data Thing = Good [(Char,Int)] | Bad String | Indifferent Leg

but also sometimes

data Thing = Good [(Char,Float)] | Bad String | Indifferent Arm

You could define

data Thing num bodypart = Good [(Char,num)] | Bad String | Indifferent bodypart

or if you want to make sure num is always numeric, you could do

data Num num => Thing num bodypart = Good [(Char,num)] | Bad String | Indifferent bodypart

and finally, you could restrict the types allowed in bodypart by defining your own class:

class Body a where
   -- some useful function(s) here

instance Body Leg where
   -- define useful function(s) on Legs
instance Body Arm
   -- define useful function(s) on Arms

data (Num num,Body bodypart) => Thing num bodypart = 
                                                             Good [(Char,num)] | Bad String | Indifferent bodypart

I'd like to discourage you from using existential types via the forall constructor or via GADTs because adding the num parameter to your data type is considerably more useful in practice, even though it takes more typing.

Constraints on type synonyms?

Note that when you use a constraint like

data (Num num) => Thing num = T [(Char,num)]

really only changes the type of the constructor T to

T :: (Num num) => [(Char,num)] -> Thing num

instead of T :: [(Char,num)] -> Thing num. This means every time you use T, there needs to be a context (Num num), but that's really what you wanted - to stop people putting data in your data type that isn't numeric.

A consequence of this fact is that you can't write

type Num num => [(Char,num)]

because there's no data constructor function T for the context (Num num) to be required on; if I have a [('4',False)], it automatically matches the type [(Char,num)] because it's a synonym. The compiler can't be going running around your code looking for instances before it decides what type something is. In the data case, it's got a constructor T that tells it the type, and it can guarantee there's a Num num instance because it checked your use of the function T. No T, no context.


This corresponds to existential quantification. In pseudo-Haskell,

type NumberList = exists a . Num a => [a]

I say «pseudo» because GHC doesn't allow introducing existential quantifiers on the fly — you need to create a separate datatype for that.

Now, most of the type you'd use NumberList to the left of the arrow, where «exists» effectively changes its meaning to «forall».

That is, instead of writing

isIncreasing :: NumberList -> Bool

which is the same as

isIncreasing :: (exists a . Num a => [a]) -> Bool

you can write

isIncreasing :: forall a . Num a => [a] -> Bool

or simply

isIncreasing :: Num a => [a] -> Bool

Of course, having a type synonym seems like less code, but it has disadvantages as well. These disadvantages, by the way, are typical for object-oriented programming, which is based on the existential approach.

For example, you want to concatenate two lists. Ordinarily you would write

(++) :: forall a . [a] -> [a] -> [a]

(where again forall is unnecessary and added for clarity). Since a is the same across the entire signature, that ensures that you are concatenating lists of the same type.

How do we concatenate two numeric lists? A signature

(++) :: NumberList -> NumberList -> NumberList

wouldn't work, since one list may contain Ints and the other may contain Doubles. And the resulting NumberList has to contain values of a single type.

Or, say, you want to find the sum of the list elements.

Usually you write

sum :: Num a => [a] -> a

Notice that the result type is the same as the type of list elements. Alas, we cannot do the same for NumberList!

sum :: NumberList -> ???

What is the result type? We could apply existential quantification there as well.

sum :: NumberList -> (exists a . Num a => a)

But now the connection between the original list type and the sum type is lost — at least for Haskell's type system. If you then decide to write a function like

multiplySum :: Integer -> [Integer] -> Integer
multiplySum x ys = x * sum ys

then you'll get a type error, because sum ys may be of any type, not necessarily of type Integer.

It would work if you pushed everything to extreme and made every type existentially-quantified — but then you'd end up essentially with another object-oriented-like language with all their problems.

(That said, there are some good use cases for existential quantification, of course.)