I pretty much understand 3/4 the rest of the language, but every time I dip my feet into using classes in a meaningful way in my code I get permantently entrenched.
Why doesn't this extremely simple code work?
data Room n = Room n n deriving Show
class HasArea a where
width :: (Num n) => a -> n
instance (Num n) => HasArea (Room n) where
width (Room w h) = w
So, room width is denoted by ints or maybe floats, I don't want to restrict it at this point. Both the class and the instance restrict the n type to Nums, but it still doesn't like it and I get this error:
Couldn't match expected type `n1' against inferred type `n'
`n1' is a rigid type variable bound by
the type signature for `width' at Dungeon.hs:11:16
`n' is a rigid type variable bound by
the instance declaration at Dungeon.hs:13:14
In the expression: w
In the definition of `width': width (Room w h) = w
In the instance declaration for `HasArea (Room n)'
So it tells me the types doesn't match, but it doesn't tell me what types it thinks they are, which would be really helpful. As a side note, is there any easy way to debug an error like this? The only way I know to do it is to randomly change stuff until it works.
The error you're getting does tell you what it thinks the type should be; unfortunately, both types are denoted by type variables, which makes it harder to see. The first line says that you gave the expression type n
, but it wanted to give it type n1
. To figure out what these are, look at the next few lines:
`n1' is a rigid type variable bound by
the type signature for `width' at Dungeon.hs:11:16
This says that n1
is a type variable whose value is known and thus can't change (is "rigid"). Since it's bound by the type signature for width
, you know it's bound by the line width :: (Num n) => a -> n
. There's another n
in scope, so this n
is renamed to n1
(width :: (Num n1) => a -> n1
). Next, we have
`n' is a rigid type variable bound by
the instance declaration at Dungeon.hs:13:14
This is telling you that Haskell found the type n
from the line instance (Num n) => HasArea (Room n) where
. The problem that's being reported is that n
, which is the type GHC computed for width (Room w h) = w
, is not the same as n1
, which is the type it expected.
The reason you're having this problem is that your definition of width
is less polymorphic than expected. The type signature of width
is (HasArea a, Num n1) => a -> n1
, which means that for each type which is an instance of HasArea
, you can represent its width with any kind of number at all. However, in your instance definition, the line width (Room w h) = w
means that width
has type Num n => Room n -> n
. Note that this is not sufficiently polymorphic: while Room n
is an instance of HasArea
, this would require width
to have the type (Num n, Num n1) => Room n -> n1
. It's this inability to unify the specific n
with the general n1
that's causing your type error.
There are a couple ways to fix it. One approach (and probably the best approach), which you can see in sepp2k's answer is to make HasArea
take a type variable of kind * -> *
; this means that rather than a
being a type itself, things like a Int
or a n
are types. Maybe
and []
are examples of types with kind * -> *
. (Ordinary types like Int
or Maybe Double
have kind *
.) This is probably the best bet.
If you have some types of kind *
which have an area (e.g., data Space = Space (Maybe Character)
, where the width
is always 1
), however, that won't work. Another way (which requires some extensions to Haskell98/Haskell2010) is to make HasArea
a multi-parameter type class:
{-# LANGUAGE MultiParamTypeClasses, FlexibleInstances #-}
data Room n = Room n n deriving Show
class Num n => HasArea a n where
width :: a -> n
instance Num n => HasArea (Room n) n where
width (Room w h) = w
Now, you pass the type of the width as a parameter to the type class itself, so width
has the type (HasArea a n, Num n) => a -> n
. A possible downside to this, though, is that you can declare instance HasArea Foo Int
and instance HasArea Foo Double
, which may be problematic. If it is, then to solve this problem, you could use functional dependencies or type families. Functional dependencies allow you to specify that given one type, the other types are uniquely determined, just as if you had an ordinary function. Using those gives the code
{-# LANGUAGE MultiParamTypeClasses, FlexibleInstances, FunctionalDependencies #-}
data Room n = Room n n deriving Show
class Num n => HasArea a n | a -> n where
width :: a -> n
instance Num n => HasArea (Room n) n where
width (Room w h) = w
The | a -> n
bit tells GHC that if it can infer a
, then it can also infer n
, since there's only one n
for every a
. This prevents the sort of instances discussed above.
Type families are more different:
{-# LANGUAGE MultiParamTypeClasses, FlexibleContexts, TypeFamilies #-}
data Room n = Room n n deriving Show
class Num (Area a) => HasArea a where
type Area a :: *
width :: a -> Area a
instance Num n => HasArea (Room n) where
type Area (Room n) = n
width (Room w h) = w
This says that in addition to having a width
function, the HasArea
class also has an Area
type (or type function, if you want to think about it that way). For every HasArea a
, you specify what the type Area a
is (which, thanks to the superclass constraint, must be an instance of Num
), and then use that type as your kind of number.
As for how to debug errors like this? Honestly, my best advice is "Practice, practice, practice." With time, you'll get more used to figuring out (a) what the errors are saying, and (b) what probably went wrong. Changing stuff randomly is one way to do that learning. The biggest piece of advice I can give, though, is to pay attention to the Couldn't match expected type `Foo' against inferred type `Bar'
lines. These tell you what the compiler computed (Bar
) and expected (Foo
) for the type, and if you can figure out precisely which types those are, that helps you figure out where the error is.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With