Search code examples
haskelltypesconstraintstype-variablesfunction-signature

Type constraints with multiple type variables in Haskell function signature


I have been searching on documentations and wikis for Haskell's type constraints in function signatures. Unfortunately, I wasn't able to find a satisfactory answer.

At this time, I consider myself as a beginner, so I would like to ask your comprehension if you see misuse of technical terms or lack of advanced knowledge on this topic.

First scenario

Consider a real example given by the function signature below:

count :: (Eq e, Num n) => e -> [e] -> n -> n

This function has the type constraints: Eq and Num, each one with exactly one variable, respectively: e and n. I could have provided a simpler signature with only one constraint too (e.g. foo :: Eq a => a -> Bool).

But what about the following?

Second scenario

Consider the hypothetical examples. I thought of them trying to represent multiple variables that belong to the same type constraint.

-- As far as I tested, the examples below are incorrect
-- and do not reflect proper Haskell syntax

foo :: (Num a b c) => use the variables here

or

foo :: Num a, b, c => use the variables here

Can we specify more than one variable in the same constraint, like I tried above? Would I have to specify them individually?

foo :: Eq a, Eq b ... => use the variables here

Is there any case in which one would use multiple type variables of the same type, what impacts does this bring?


Edit - May 1, 2023: I accepted Ben’s answer because it contains a thorough explanation and is very detailed. I really found it to be clarifying and constructive as he complements the discussion, pointing out to Joseph’s answers, which is extremely relevant.


Solution

  • The syntax for constraints is that left of the => arrow you must have a single thing of kind Constraint.

    () is a Constraint (the empty one that is always satisfied and provides no capabilities). Any single type class constraint is obviously a Constraint. A pair of constraints is also a Constraint, and must be written (c1, c2). Likewise triples, quadruples, etc. If you want multiple constraints, you put them in a comma-separated list surrounded by parentheses.

    Basic type-class names like Eq, Num, etc are Constraint producers. They have kinds Type -> Constraint, i.e. you apply them to a type (most commonly a variable) to make a Constraint. So I can apply Num to the type variable a to make a Constraint like Num a.

    But Num a b c doesn't make sense; it is requiring Num to be something that can be applied to three types to make a Constraint. Such a thing would have kind Type -> Type -> Type -> Constraint, and things like that can exist so we can't have Num a b c automatically apply Num to all 3 variables without interfering with the syntax for applying multi-paramter type classes to multiple variables.

    If you want all of a, b, and c to have Num constraints, the "builtin" Haskell syntax for that is simply (Num a, Num b, Num c). A little repetitive perhaps, but since type class constraints declare the interface you are using to work with values of types a, b, and c it's important information to be clear about. It's also uncommon for a Haskell function to have a huge number of type variables, so more than a handful of repetitions of a type class name in a signature is an extremely uncommon problem anyway.

    Nevertheless, there are ways to address this if you want using more advanced Haskell. Constraints (and the things that produce them, like Num, Eq, etc) are perfectly ordinary type-level entities in Haskell1, so any feature that can be used to manipulate types can be used to manipulate Constraints. So even though there's no builtin syntax for applying a single constraint-producer (like Num) to multiple types, you can make something to do that job. The other answers have shown a couple of ways of doing this. If you're relatively early in your Haskell learning I'd probably recommend you just stick with writing out all the constraints manually for now, but up to you.


    1 At least, they are with the ConstraintKinds language extension, but this is extremely well-accepted and now enabled by default on compilers that support the GHC2021 language.


    Is there any case in which one would use multiple type variables of the same type, what impacts does this bring?

    Are you talking about using multiple type variables with the type class constraint? Say like pairEqual :: (Eq a, Eq b) => (a, b) -> (a, b) -> Bool? Yes, that is sometimes done.

    Each variable (and constraint) means the same thing as it always does. The caller of pairEqual can choose a to be any type they like (that has an Eq instance), and can choose b to be any type they like (that has an Eq instance). The code of pairEqual can use the interface of the Eq class for values of type a and b (i.e. it can test equality of a values, and can also test equality of b values).

    But the important thing is that when multiple type variables are in use these choices and interfaces are completely independent of each other. The caller of pairEqual can choose the types a and b completely independently; it choose a to be Char and b to be Maybe Bool (by calling it as pairEqual ('a', Just False) ('a', Nothing), for example). a and b don't have to have any relationship to each other.

    And the code implementing pairEqual can call == on two a values to test whether one value of type a is equal to another value of type a, and it can do the same with b values, but it has no ability to test whether an a value is equal to a b value. The two Eq interfaces it has access to are independent from one another.

    So this is quite different in effect from if we had pairEqual :: Eq a => (a, a) -> (a, a) -> Bool. Here there's only one type variable for the caller to choose; both elements of both pairs have to be the same type. This gives more freedom to the code implementing pairEqual; it knows that both elements of both pairs are all of one single type and all work with the Eq interface it has. So it can test whether the first element of the first pair is equal to the second element of the second pair, if it wants. Our two-type-variable pairEqual from before couldn't do that. But it gives less freedom to people calling pairEqual; now pairEqual ('a', Just False) ('a', Nothing) is invalid, since there's no type that can be chosen for the single type variable a that allows the values 'a' and Just False.

    So using more or fewer type variables really depends on what you want to do. Using more can be "better" in that it gives more freedom to the callers of your function (i.e. it makes your function useful in more situations). The restrictions on the code of your function of using more independent types can also be useful for making sure you don't accidentally write the wrong code. But there usually just isn't a lot you can do with values of completely independent types, so you won't be able to just give every input value its own type variable.