为什么 Num 可以像小数一样起作用? [英] Why can a Num act like a Fractional?
问题描述
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?
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.
- For
forall a. t
, the caller gets to choose a type, then they continue with protocolt
(wherea
has been replaced with the caller's choice everywhere int
). - For
Foo a => t
, the caller must provide proof to the implementer thata
is an instance ofFoo
. Then they continue with protocolt
. - For
t1 -> t2
, the caller gets to choose a value of typet1
(e.g. by running protocolt1
with the roles of implementer and caller switched). Then they continue with protocolt2
. - For any type
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 likeInt
or a bare type variable likea
), 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:
- Caller chooses a type
a
. - Caller proves it is
Fractional
. - Implementer provides a value of type
a
.
The implementation says:
Implementer starts the
idNum
protocol as the caller. So, she must:- Choose a type
a
. She quietly makes the same choice as her caller did. - Prove that
a
is an instance ofNum
. This is no problem, because she actually knows thata
isFractional
, and this impliesNum
. - Provide a value of type
a
. Here she choosesvalFrac
. To be complete, she must then show thatvalFrac
has the typea
.
- Choose a type
So the implementer now runs the
valFrac
protocol. She:- Chooses a type
a
. Here she quietly chooses the type thatidNum
is expecting, which happens to coincidentally be the same as the type that her caller chose fora
. - Prove that
a
is an instance ofFractional
. She uses the same proof her caller did. - The implementer of
valFrac
then promises to provide a value of typea
, as needed.
- Chooses a type
For completeness, here is the analogous discussion for applyIdFrac
. The protocol says:
- Caller chooses a type
a
. - Caller proves that
a
isFractional
. - Implementer must provide a value of type
a
.
The implementation says:
Implementer will execute the
idFrac
protocol. So, she must:- Choose a type. Here she quietly chooses whatever her caller chose.
- Prove that
a
isFractional
. She passes on her caller's proof of this. - Choose a value of type
a
. She will execute thevalNum
protocol to do this; and we must check that this produces a value of typea
.
During the execution of the
valNum
protocol, she:- Chooses a type. Here she chooses the type that
idFrac
expects, namelya
; this also happens to be the type her caller chose. - Prove that
Num a
holds. This she can do, because her caller supplied a proof thatFractional a
, and you can extract a proof ofNum a
from a proof ofFractional a
. - The implementer of
valNum
then provides a value of typea
, as needed.
- Chooses a type. Here she chooses the type that
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.
这篇关于为什么 Num 可以像小数一样起作用?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!