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, for I believe it explains thoroughly and in rich details about my questions. I really found it to be clarifying and very constructive as he also complements pointing out to Joseph’s answers, which are relevant to the context.
The syntax for constraints is that left of the
=>arrow you must have a single thing of kindConstraint.()is aConstraint(the empty one that is always satisfied and provides no capabilities). Any single type class constraint is obviously aConstraint. A pair of constraints is also aConstraint, 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 areConstraintproducers. They have kindsType -> Constraint, i.e. you apply them to a type (most commonly a variable) to make aConstraint. So I can applyNumto the type variableato make aConstraintlikeNum a.But
Num a b cdoesn't make sense; it is requiringNumto be something that can be applied to three types to make aConstraint. Such a thing would have kindType -> Type -> Type -> Constraint, and things like that can exist so we can't haveNum a b cautomatically applyNumto all 3 variables without interfering with the syntax for applying multi-paramter type classes to multiple variables.If you want all of
a,b, andcto haveNumconstraints, 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 typesa,b, andcit'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, likeNum,Eq, etc) are perfectly ordinary type-level entities in Haskell1, so any feature that can be used to manipulate types can be used to manipulateConstraints. So even though there's no builtin syntax for applying a single constraint-producer (likeNum) 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
ConstraintKindslanguage extension, but this is extremely well-accepted and now enabled by default on compilers that support theGHC2021language.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
pairEqualcan chooseato be any type they like (that has anEqinstance), and can choosebto be any type they like (that has anEqinstance). The code ofpairEqualcan use the interface of theEqclass for values of typeaandb(i.e. it can test equality ofavalues, and can also test equality ofbvalues).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
pairEqualcan choose the typesaandbcompletely independently; it chooseato beCharandbto beMaybe Bool(by calling it aspairEqual ('a', Just False) ('a', Nothing), for example).aandbdon't have to have any relationship to each other.And the code implementing
pairEqualcan call==on twoavalues to test whether one value of typeais equal to another value of typea, and it can do the same withbvalues, but it has no ability to test whether anavalue is equal to abvalue. The twoEqinterfaces 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 implementingpairEqual; it knows that both elements of both pairs are all of one single type and all work with theEqinterface 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-variablepairEqualfrom before couldn't do that. But it gives less freedom to people callingpairEqual; nowpairEqual ('a', Just False) ('a', Nothing)is invalid, since there's no type that can be chosen for the single type variableathat allows the values'a'andJust 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.