Reputation: 2176
I am confused with classes in Haskell as follows.
I can define a function that takes an Integral argument, and successfully supply it with a Num argument:
gi :: Integral a => a -> a
gi i = i
gin = gi (3 :: Num a => a)
I can define a function that takes a Num argument, and successfully supply it with an Integral argument:
fn :: Num a => a -> a
fn n = n
fni = fn (3 :: Integral a => a)
I can define an Integral value and assign a Num to it
i :: Integral a => a
i = (3 :: Num a => a)
But if I try to define a Num value, then I get a parse error if I assign an Integral value to it
- this doesn't work
n :: Num a => a
n = (3 :: Integral a => a)
Maybe I am being confused by my OO background. But why do function variables appear to let you go 'both ways' i.e. can provide a value of a subclass when a superclass is 'expected' and can provide a value of a superclass when a subclass is expected, whereas in value assignment you can provide a superclass to a subclass value but can't assign a subclass to a superclass value?
For comparison, in OO programming you can typically assign a child value to a parent type, but not vice-versa. In Haskell, the opposite appears to be the case in the second pair of examples.
Upvotes: 2
Views: 77
Reputation: 80744
The first two examples don't actually have anything to do with the relationship between Num
and Integral
.
Take a look at the type of gin
and fni
. Let's do it together:
> :t gin
gin :: Integer
> :t fni
fni :: Integer
What's going on? This is called "type defaulting".
Technically speaking, any numeric literal like 3
or 5
or 42
in Haskell has type Num a => a
. So if you wanted it to just be an integer number dammit, you'd have to always write 42 :: Integer
instead of just 42
. This is mighty inconvenient.
So to work around that, Haskell has certain rules that in certain special cases prescribe concrete types to be substituted when the type comes out generic. And in case of both Num
and Integral
the default type is Integer
.
So when the compiler sees 3
, and it's used as a parameter for gi
, the compiler defaults to Integer
. That's it. Your additional constraint of Num a
has no further effect, because Integer
is, in fact, already an instance of Num
.
With the last two examples, on the other hand, the difference is that you explicitly specified the type signature. You didn't just leave it to the compiler to decide, no! You specifically said that n :: Num a => a
. So the compiler can't decide that n :: Integer
anymore. It has to be generic.
And since it's generic, and constrained to be Num
, an Integral
type doesn't work, because, as you have correctly noted, Num
is not a subclass of Integral
.
You can verify this by giving fni
a type signature:
-- no longer works
fni :: Num a => a
fni = fn (3 :: Integral a => a)
Wait, but shouldn't n
still work? After all, in OO this would work just fine. Take C#:
class Num {}
class Integral : Num {}
class Integer : Integral {}
Num a = (Integer)3
// ^ this is valid (modulo pseudocode), because `Integer` is a subclass of `Num`
Ah, but this is not a generic type! In the above example, a
is a value of a concrete type Num
, whereas in your Haskell code a
is itself a type, but constrained to be Num
. This is more like a C# interface than a C# class.
And generic types (whether in Haskell or not) actually work the other way around! Take a value like this:
x :: a
x = ...
What this type signature says is that "Whoever has a need of x
, come and take it! But first name a type a
. Then the value x
will be of that type. Whichever type you name, that's what x
will be"
Or, in plainer terms, it's the caller of a function (or consumer of a value) that chooses generic types, not the implementer.
And so, if you say that n :: Num a => a
, it means that value n
must be able to "morph" into any type a
whatsoever, as long as that type has a Num
instance. Whoever will use n
in their computation - that person will choose what a
is. You, the implementer of n
, don't get to choose that.
And since you don't get to choose what a
is, you don't get to narrow it down to be not just any Num
, but an Integral
. Because, you know, there are some Num
s that are not Integral
s, and so what are you going to do if whoever uses n
chooses one of those non-Integral
types to be a
?
In case of i
this works fine, because every Integral
must also be Num
, and so whatever the consumer of i
chooses for a
, you know for sure that it's going to be Num
.
Upvotes: 5