csh
csh

Reputation: 65

Using Class Constraints in Type Annotations for Expressions in Haskell Doesn't Work as Expected

I'm a Haskell newbie currently learning about expressions and types. I recently learned that it's possible to use class constraints when specifying types in Haskell. To experiment with this, I tried the following code in GHCi:

ghci> a = 3 :: Num a => a
ghci> :t a
a :: Num a => a

ghci> :i Num
type Num :: * -> Constraint
class Num a where
  (+) :: a -> a -> a
  (-) :: a -> a -> a
  (*) :: a -> a -> a
  negate :: a -> a
  abs :: a -> a
  signum :: a -> a
  fromInteger :: Integer -> a
  {-# MINIMAL (+), (*), abs, signum, fromInteger, (negate | (-)) #-}
        -- Defined in ‘GHC.Num’
instance Num Double -- Defined in ‘GHC.Float’
instance Num Float -- Defined in ‘GHC.Float’
instance Num Int -- Defined in ‘GHC.Num’
instance Num Integer -- Defined in ‘GHC.Num’
instance Num Word -- Defined in ‘GHC.Num’

ghci> :t (==)
(==) :: Eq a => a -> a -> Bool

ghci> a == 5
False

I don’t understand why the expression a == 5 works without any errors. The variable a has the type Num a => a, and since the Num class does not define the (==) function, I expected a type error.

Could someone explain why this works?

Upvotes: 1

Views: 55

Answers (2)

chi
chi

Reputation: 116174

You raise a good point! Indeed, it can be argued that your code should not type check. And, by the same argument, 3 == 5 should also fail type checking, since 3 and 5 could be of a type that does not support ==.

However... this is really harsh. Surely most programmers would expect 3 == 5 to be OK. Even if, as you mentioned, there would be valid theoretical reasons for that to fail.

The Haskell designers chose a compromise, adding a special case to the type system. For some "special" type classes like Num, instead of reporting an error saying "your code does not tell me what specific numeric type a to use, and this causes issues elsewhere", the compiler tries some default numeric types e.g. a ~ Integer and moves on.

From an elegance point of view, this defaulting mechanism is a little wart. Worse, the exact type defaulting rules are not the same in GHCi and in GHC! You will find that at the GHCi prompt there is more magic defaulting going on so to avoid stressing the user too much, while GHC is not so lenient (unless you ask it to be).

However, from a pragmatic point of view, it makes some sense. No one really wants print 42 to fail.

Upvotes: 1

willeM_ Van Onsem
willeM_ Van Onsem

Reputation: 477462

ghci will ad-hoc try to assign a type to a. If you write:

a = 3 :: Num a => a

You actually have written something like:

a = fromInteger 3

which thus can be specialized to an Integer, Double, etc.

Now in the context of a == 5, it thus requires a to be (Num a, Eq a) => a.

It will perform type defaulting and in thus case use Integer. It thus checks for:

fromInteger 3 == fromInteger 5

and specializes the two to Integer and then runs the (==) function on these.

Types in Haskell are a bit "opposite" to what those are in for example Java. If in Java you write:

MyInterface foo;

It means that we only know foo is an instance of MyInterface.

Whereas in Haskell if we write:

x :: Num a => a

It means that x is of a type a for which Num a holds. If we later use x in another context, it can require extra type constraints.

If we thus write a function:

eq5 :: (Num a, Eq a) => a -> Bool
eq5 x = x == 5

Haskell thus promises that it will work for all types a for which Num a and Eq a are satisfied.

Upvotes: 2

Related Questions