Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Polymorphism: a constant versus a function

I'm new to Haskell and come across a slightly puzzling example for me in the Haskell Programming from First Principles book. At the end of Chapter 6 it suddenly occurred to me that the following doesn't work:

constant :: (Num a) => a
constant = 1.0

However, the following works fine:

f :: (Num a) => a -> a
f x = 3*x

I can input any numerical value for x into the function f and nothing will break. It's not constrained to taking integers. This makes sense to me intuitively. But the example with the constant is totally confusing to me.

Over on a reddit thread for the book it was explained (paraphrasing) that the reason why the constant example doesn't work is that the type declaration forces the value of constant to only be things which aren't more specific than Num. So trying to assign a value to it which is from a subclass of Num like Fractional isn't kosher.

If that explanation is correct, then am I wrong in thinking that these two examples seem completely opposites of each other? In one case, the type declaration forces the value to be as general as possible. In the other case, the accepted values for the function can be anything that implements Num.

Can anyone set me straight on this?

like image 661
gogurt Avatar asked Mar 23 '26 05:03

gogurt


2 Answers

It can sometimes help to read types as a game played between two actors, the implementor of the type and the user of the type. To do a good job of explaining this perspective, we have to introduce something that Haskell hides from you by default: we will add binders for all type variables. So your types would actually become:

constant :: forall a. Num a => a
f :: forall a. Num a => a -> a

Now, we will read type formation rules thusly:

  • forall a. t means: the caller chooses a type a, and the game continues as t
  • c => t means: the caller shows that constraint c holds, and the game continues as t
  • t -> t' means: the caller chooses a value of type t, and the game continues as t'
  • t (where t is a monomorphic type such as a bare variable or Integer or similar) means: the implementor produces a value of type a

We will need a few other details to truly understand things here, so I will quickly say them here:

  • When we write a number with no decimal points, the compiler implicitly converts this to a call to fromInteger applied to the Integer produced by parsing that number. We have fromInteger :: forall a. Num a => Integer -> a.
  • When we write a number with decimal points, the compiler implicitly converts this to a call to fromRational applied to the Rational produced by parsing that number. We have fromRational :: forall a. Fractional a => Rational -> a.
  • The Num class includes the method (*) :: forall a. Num a => a -> a -> a.

Now let's try to walk through your two examples slowly and carefully.

constant :: forall a. Num a => a
constant = 1.0 {- = fromRational (1 % 1) -}

The type of constant says: the caller chooses a type, shows that this type implements Num, and then the implementor must produce a value of that type. Now the implementor tries to play his own game by calling fromRational :: Fractional a => Rational -> a. He chooses the same type the caller did, and then makes an attempt to show that this type implements Fractional. Oops! He can't show that, because the only thing the caller proved to him was that a implements Num -- which doesn't guarantee that a also implements Fractional. Dang. So the implementor of constant isn't allowed to call fromRational at that type.

Now, let's look at f:

f :: forall a. Num a => a -> a
f x = 3*x {- = fromInteger 3 * x -}

The type of f says: the caller chooses a type, shows that the type implements Num, and chooses a value of that type. The implementor must then produce another value of that type. He is going to do this by playing his own game with (*) and fromInteger. In particular, he chooses the same type the caller did. But now fromInteger and (*) only demand that he prove that this type is an instance of Num -- so he passes off the proof the caller gave him of this and saves the day! Then he chooses the Integer 3 for the argument to fromInteger, and chooses the result of this and the value the caller handed him as the two arguments to (*). Everybody is satisfied, and the implementor gets to return a new value.

The point of this whole exposition is this: the Num constraint in both cases is enforcing exactly the same thing, namely, that whatever type we choose to instantiate a at must be a member of the Num class. It's just that in the definition constant = 1.0 being in Num isn't enough to do the operations we've written, whereas in f x = 3*x being in Num is enough to do the operations we've written. And since the operations we've chosen for the two things are so different, it should not be too surprising that one works and the other doesn't!

like image 94
Daniel Wagner Avatar answered Mar 25 '26 21:03

Daniel Wagner


When you have a polymorphic value, the caller chooses which concrete type to use. The Haskell report defines the type of numeric literals, namely:

integer and floating literals have the typings (Num a) => a and (Fractional a) => a, respectively

3 is an integer literal so has type Num a => a and (*) has type Num a => a -> a -> a so f has type Num a => a -> a.

In contrast, 3.0 has type Fractional a => a. Since Fractional is a subclass of Num your type signature for constant is invalid since the caller could choose a type for a which is Num but not Fractional e.g. Int or Integer.

like image 22
Lee Avatar answered Mar 25 '26 20:03

Lee



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!