I’ve been reading about the renaming of pre-existing types using the haskell’s type
and, as much as I could understand, the type
is only used for renaming concrete types, like Int, Integer, Char, Double, etc. So it is not used for renaming typeclasses, like Eq
, Integral
, Ord
, etc. (Again, this is just as much, as I could understand, I don’t have 100% certainty if I am correct)
But here is the thing: I tested renaming Num
and it worked!
type NumRenamed = Num
sillyNewFun :: NumRenamed a => a
sillyNewFun = 2
sillyNewFun2 :: NumRenamed a => a
sillyNewFun2 = 3
and it compiles fine, but when I check the type of :t (sillyNewFun + sillyNewFun2)
, for example, it returns me (sillyNewFun + sillyNewFun2) :: Num a => a
. Notice that, although it worked, internally, GHC/GHCi still treats NumRenamed
like Num
(at least that’s my guess, let me know if I am right, though).
But if we have tried doing something like:
type NumRenamed = (Num,Num)
sillyNewFun :: NumRenamed
sillyNewFun = (2,3)
We would have gotten a rather long error message, while if we had tried doing the exact same thing with a concrete type, nothing would have exploded. This is the error message I am talking about:
teste5.hs:1:20: error:
• Expecting one more argument to ‘Num’
Expected a type, but ‘Num’ has kind ‘* -> Constraint’
• In the type ‘(Num, Num)’
In the type declaration for ‘NumRenamed’
|
1 | type NumRenamed = (Num,Num)
| ^^^
teste5.hs:1:24: error:
• Expecting one more argument to ‘Num’
Expected a type, but ‘Num’ has kind ‘* -> Constraint’
• In the type ‘(Num, Num)’
In the type declaration for ‘NumRenamed’
|
1 | type NumRenamed = (Num,Num)
| ^^^
Failed, no modules loaded.
So why this happens ? I thought we couldn’t rename typeclasses, but we kinda can as shown on the first code example, but when we try to push further and make a tuple of Num’s we can’t. So, my question is: Why the first example worked and the last one didn’t and, if there is a way to fix it, how can we do it ?
Sidenote:
If we wanted to compare the renaming of the typeclass of the first example with the renaming of a concrete type, we could make:
type NumRenamed = Int
sillyNewFun :: NumRenamed
sillyNewFun = 2
sillyNewFun2 :: NumRenamed
sillyNewFun2 = 3
and then :t (sillyNewFun + sillyNewFun2)
returns (sillyNewFun + sillyNewFun2) :: NumRenamed
. Notice that when we used a concrete type, internally GHC/GHCi recognizes the new type, so it actually says that this expression is of type NumRenamed
, not Int
, differently from the previous example when we redifined the Num
, where it said the expression was still a Num
.
1
Let’s examine the kind of Num
in ghci:
ghci> :set -XNoStarIsType
ghci> :kind Num
Num :: Type -> Constraint
(NoStarIsType
is to use the kind Type
instead of the kind *
for printing, which IMHO is clearer.)
So Num
can be understood as a type-level “Constraint
constructor” that takes a type like Int
and returns a Constraint
like Num Int
.
NumRenamed
has the same kind. You could also have been more explicit with the type argument, like:
ghci> type NumRenamed a = Num a
ghci> :kind NumRenamed
NumRenamed :: Type -> Constraint
But here
type NumRenamed = (Num,Num)
You are not defining something of kind Type -> Constraint
. The Num
s in the tuple of constraints are not applied to some type argument. You end up trying to define a weird type-level tuple of unapplied “Constraint
constructors” that isn’t something useable as a function constraint.
Instead, you could be more explicit with the type argument:
ghci> type NumRenamed a = (Num a, Num a)
ghci> :kind NumRenamed
NumRenamed :: Type -> Constraint
By the way, if for some reason you really wanted the weird type-level tuple, you could do it like this:
ghci> :set -XDataKinds
ghci> type NumRenamed' = '(Num, Num)
ghci> :kind NumRenamed'
NumRenamed' :: (Type -> Constraint, Type -> Constraint)
Term-level expressions have types, e.g. True :: Bool
, const True :: Int -> Bool
.
Type-level expressions have kinds, e.g. Bool :: Type
, Maybe :: Type -> Type
.
It was not so complex in the past, but Haskell evolved quite a lot. In GHCi you can ask for a type with :t
, and for a kind with :k
.
Now, type classes like Num
are “type-level expressions”, and have a kind as well: Num :: Type -> Constraint
. We also have Num Int :: Constraint
.
Expressions of kind Constraint
is what we write in types, e.g.
foo :: Num a => a -> a
foo x = x+1
uses a :: Type
, Num a :: Constraint
, a -> a :: Type
.
The syntax type T = ...
can now bind type level expressions.
import Data.Kind
type N :: Type -> Constraint
type N = Num
We also have more complex (and confusing) uses:
-- pair of type classes
type P :: (Type -> Constraint, Type -> Constraint)
type P = '(Num, Show)
-- pair of constraints
type Q :: (Constraint, Constraint)
type Q = '(Num Int, Show Bool)
-- This is the "AND" of two constraints, not a pair!
type R :: Type -> Constraint
type R a = (Num a, Show a) -- note: no ' here!
(By the way, the syntax (..,..)
is vastly overloaded in Haskell, leading to great confusion at first. Then we also have '(..,..)
to make things worse. I wish we used distinct syntax for each case.)
Note that type ...
does not really define new types or type classes, but it is only an alias. In particular, the last example above does not allow to use R
without applying to an argument. We get an error otherwise:
type S :: (Type -> Constraint, Type -> Constraint)
type S = '(Num, R)
* The type synonym `R' should have 1 argument, but has been given none
* In the type synonym declaration for `S'
This is a bit counterintuitive. Just remember that aliases type ...
do not allow you to do anything you can not do without, by replacing their definition at each use point.
After this
type NumRenamed = (Num,Num)
NumRenamed
is simply a name for the type expression (Num,Num)
and will in most contexts behave exactly the same as if you had written (Num,Num)
instead.
So this fails:
sillyNewFun :: NumRenamed
sillyNewFun = (2,3)
for exactly the same reason that this fails:
sillyNewFun :: (Num,Num)
sillyNewFun = (2,3)
The problem has nothing to do with not being able to use type
to declare a new name for a type class, the problem is simply that you can’t use type class names the way you were trying to use them. (Num,Num)
is not a valid way to write the type of a pair of numbers, so using a type
alias for (Num,Num)
doesn’t work either.
Take another look at your first example:
type NumRenamed = Num
sillyNewFun :: NumRenamed a => a
sillyNewFun = 2
sillyNewFun2 :: NumRenamed a => a
sillyNewFun2 = 3
Here it seems you understood that a type class like Num
(referenced by the alias NumRenamed
) needs to be used in a constraint, to the left of the =>
arrow. sillyNeeFun2
is not given the type Num
or NumRenamed
, but NumRenamed a => a
. Translated to English this says “sillyNewFun2
can be used as any type a
that is a member of the type class NumRenamed
” (and of course NumRenamed
is another name you have assigned to the type class Num
, so that means any type that is a member of Num
).
But then with your type NumRenamed = (Num, Num)
example you appear to have forgotten that. Now sillyNewFun :: NumRenamed
translates into English as “sillyNewFun
is a NumRenamed
“, but NumRenamed
is a name for (Num, Num)
, which isn’t using the Num
class to constrain the set of types a variable ranges over, it’s instead a pair of (unapplied!) type class constraints; it doesn’t really make any sense.
The syntax you’re looking for would be:
pairSameType :: Num a => (a, a)
pairSameType = (2, 3)
or possibly:
pairPossiblyDifferntTypes :: (Num a, Num b) => (a, b)
pairPossiblyDifferntTypes = (2, 3)
And note that these two are quite different, which is one reason why we use a more verbose syntax with type variables; if we just used (Num, Num)
to mean “the type of a pair of values that are both of types in the Num class” there would be no easy way to distinguish these different use cases.
You can then use type
aliases to rename some parts of those type expressions if you wish, although if you want to bury constraints like Num a
under a type alias the rules are a little more complicated than simply imagining that the RHS of the alias is directly replacing everywhere you write the alias name.
1