Reputation: 1047
As expected, this works fine:
foo :: Fractional a => a
foo = undefined -- datum
bar :: Num a => a -> a
bar a = undefined -- function
baz :: Fractional a => a
baz = bar foo -- application
This works as expected because every Fractional
is also a Num
.
So as expected, we can pass a Fractional
argument into a Num
parameter.
On the other hand, the following works too. I don't understand why.
foo :: Fractional a => a -> a
foo a = undefined -- function
bar :: Num a => a
bar = undefined -- datum
baz :: Fractional a => a
baz = foo bar -- application
Works unexpectedly! There are Num
s that are not Fractionals
.
So why can I pass a Num
argument into a Fractional
parameter? Can you explain?
Upvotes: 4
Views: 286
Reputation: 71099
TL;DR: it is not the case that Num a => a
is a Num
value, but rather it is a definition that can be a value of any type of Num
, whatever that type is, specifically, as determined by each specific place where it is used.
We define it first, and we use it, later.
And if we've defined it generally, so that it can be used at many different specific types, we can use it later at many different use sites, which will each demand a specific type of value to be provided for it by our definition. As long as that specific type conforms to the type constraints as per the definition and the use site.
That's what being a polymorphic definition is all about.
It is not a polymorphic value. That is a concept from the dynamic world, but ours is a static one. The types in Haskell are not decided at run time. They are known upfront.
Here's what happens:
> numfunc :: Num a => a -> a; numfunc = undefined
> fraval :: Fractional a => a; fraval = undefined
> :t numfunc fraval
numfunc fraval :: Fractional a => a
numfunc
demands its argument to be in Num
. fraval
is a polymorphic definition, able to provide a datum of any type which is in Fractional
as might be demanded of it by a particular use. Whatever it will be, since it's in Fractional
, it's guaranteed to also be in Num
, so it is acceptable by numfunc
.
Since we now know that a
is in Fractional
(because of fraval
), the type of the whole application is now known to be in Fractional
as well (because of the type of numfunc
).
Technically,
fraval :: Fractional a => a -- can provide any Fractional
numfunc :: Num a => a -> a -- is able to process a Num
-------------------------------------------------
numfunc fraval :: (Num a, Fractional a) => a -- can provide a Fractional
And (Num a, Fractional a)
is simplified to the intersection of the type classes, i.e. just Fractional a
.
This of course means that if there's nothing else in the rest of the code somewhere, further specifying the types, we'll get an ambiguous type error (unless some type defaulting kicks in). But there might be. For now this is acceptable, and has a type -- a polymorphic type, meaning, something else will have to further specify it somewhere in the rest of the code, at any particular use site where it appears. So for now, as a general polymorphic definition, this is perfectly acceptable.
Next,
> frafunc :: Fractional a => a -> a; frafunc = undefined
> numval :: Num a => a; numval = undefined
> :t frafunc numval
frafunc numval :: Fractional a => a
frafunc
demands its type to be in Fractional
. numval
is able to provide a datum of whatever type is demanded of it as long as that type is in Num
. So it's perfectly happy to oblige any demand for a Fractional
type of value. Of course something else in the code will have to further specialize the types, but whatever. For now it's all good.
Technically,
numval :: Num a => a -- can provide any Num
frafunc :: Fractional a => a -> a -- is able to process a Fractional
-------------------------------------------------
frafunc numval :: (Num a, Fractional a) => a -- can provide any Fractional
(I post this answer because I think the simplest things can be a stumbling block for beginners, and these simplest things can be taken for granted without even noticing, by the experts. As the saying goes, we don't know who discovered water, but it sure wasn't a fish.)
Upvotes: 0
Reputation: 116174
The type a
in baz :: Fractional a => a
is chosen by whoever calls baz
. It is their responsibility to guarantee that their choice of a
type is in the Fractional
class. Since Fractional
is a subclass of Num
, the type a
must therefore be also a Num
. Hence, baz
can use both foo
and bar
.
In other words, because of the subclass relation, the signature
baz :: Fractional a => a
is essentially equivalent to
baz :: (Fractional a, Num a) => a
Your second example is actually of the same kind as the first one, it does not matter which one between foo, bar
is the function and which one is the argument. You might also consider this:
foo :: Fractional a => a
foo = undefined
bar :: Num a => a
bar = undefined
baz :: Fractional a => a
baz = foo + bar -- Works
Upvotes: 6
Reputation: 153102
chi's answer gives a great high-level explanation of what's happening. I thought it might also be fun to give a slightly more low-level (but also more mechanical) way to understand this, so that you might be able to approach other similar problems, turn a crank, and get the right answer. I'm going to talk about types as a sort of protocol between the user of the value of that type and the implementer.
forall a. t
, the caller gets to choose a type, then they continue with protocol t
(where a
has been replaced with the caller's choice everywhere in t
).Foo a => t
, the caller must provide proof to the implementer that a
is an instance of Foo
. Then they continue with protocol t
.t1 -> t2
, the caller gets to choose a value of type t1
(e.g. by running protocol t1
with the roles of implementer and caller switched). Then they continue with protocol t2
.t
(that is, at any time), the implementer can cut the protocol short by just producing a value of the appropriate type. If none of the rules above apply (e.g. if we have reached a base type like Int
or a bare type variable like a
), the implementer must do so.Now let's give some distinct names to your terms so we can differentiate them:
valFrac :: forall a. Fractional a => a
valNum :: forall a. Num a => a
idFrac :: forall a. Fractional a => a -> a
idNum :: forall a. Num a => a -> a
We also have two definitions we want to explore:
applyIdNum :: forall a. Fractional a => a
applyIdNum = idNum valFrac
applyIdFrac :: forall a. Fractional a => a
applyIdFrac = idFrac valNum
Let's talk about applyIdNum
first. The protocol says:
a
.Fractional
.a
.The implementation says:
Implementer starts the idNum
protocol as the caller. So, she must:
a
. She quietly makes the same choice as her caller did.a
is an instance of Num
. This is no problem, because she actually knows that a
is Fractional
, and this implies Num
.a
. Here she chooses valFrac
. To be complete, she must then show that valFrac
has the type a
.So the implementer now runs the valFrac
protocol. She:
a
. Here she quietly chooses the type that idNum
is expecting, which happens to coincidentally be the same as the type that her caller chose for a
.a
is an instance of Fractional
. She uses the same proof her caller did.valFrac
then promises to provide a value of type a
, as needed.For completeness, here is the analogous discussion for applyIdFrac
. The protocol says:
a
.a
is Fractional
.a
.The implementation says:
Implementer will execute the idFrac
protocol. So, she must:
a
is Fractional
. She passes on her caller's proof of this.a
. She will execute the valNum
protocol to do this; and we must check that this produces a value of type a
.During the execution of the valNum
protocol, she:
idFrac
expects, namely a
; this also happens to be the type her caller chose.Num a
holds. This she can do, because her caller supplied a proof that Fractional a
, and you can extract a proof of Num a
from a proof of Fractional a
.valNum
then provides a value of type a
, as needed.With all the details on the field, we can now try to zoom out and see the big picture. Both applyIdNum
and applyIdFrac
have the same type, namely forall a. Fractional a => a
. So the implementer in both cases gets to assume that a
is an instance of Fractional
. But since all Fractional
instances are Num
instances, this means the implementer gets to assume both Fractional
and Num
apply. This makes it easy to use functions or values that assume either constraint in the implementation.
P.S. I repeatedly used the adverb "quietly" for choices of types needed during the forall a. t
protocol. This is because Haskell tries very hard to hide these choices from the user. But you can make them explicit if you like with the TypeApplications
extension; choosing type t
in protocol f
uses the syntax f @t
. Instance proofs are still silently managed on your behalf, though.
Upvotes: 7
Reputation: 120731
Works as expected because every
Fractional
is also aNum
.
That is correct, but it's important to be precise about what this means. It means this: every type in the Fractional
class is also in the Num
class. It does not mean what someone with an OO or dynamic background might understand: “every value in a Num
type is also in a Fractional
type”. If this were the case, then your reasoning would make sense: then the Num
value bar
would be insufficiently general to be used in the foo
function.
...or actually it wouldn't be, because in an OO language the number hierarchy would work in the other direction – other languages usually allow you to cast any numerical value to a fractional one, but the other direction would in these languages incur round, which reasonably strongly typed ones won't automatically do!
In Haskell, you need to worry about none of this, because there are never any implicit type conversions. bar
and foo
work on the exact same type, that this type happens a variable a
is secondary. Now, both bar
and foo
constrain this single type in different ways, but because it's the same type that's constrained you simply get a combination (Num a, Fractional a)
of both constraints, which due to Num a => Fractional a
is equivalent to Fractional a
alone.
Upvotes: 2