haskelltypestypeclasstype-constraints

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?


Solution

  • 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.