Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

I can't seem to figure out type variables mixed with classes

Tags:

haskell

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.

like image 763
David McHealy Avatar asked Jan 11 '11 00:01

David McHealy


1 Answers

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.

like image 142
Antal Spector-Zabusky Avatar answered Oct 08 '22 05:10

Antal Spector-Zabusky