Why can a Num act like a Fractional?

前端 未结 3 1466
一个人的身影
一个人的身影 2020-12-03 23:54

As expected, this works fine:

valFrac :: Fractional a => a
valFrac = undefined

fNum :: Num a => a -> a
fNum a = undefined

resFrac :: Fractional a          


        
相关标签:
3条回答
  • 2020-12-04 00:29

    Works as expected because every Fractional is also a Num.

    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.

    0 讨论(0)
  • 2020-12-04 00:49

    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 protocol t (where a has been replaced with the caller's choice everywhere in t).
    • For Foo a => t, the caller must provide proof to the implementer that a is an instance of Foo. Then they continue with protocol t.
    • For 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.
    • 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 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:

    1. Caller chooses a type a.
    2. Caller proves it is Fractional.
    3. Implementer provides a value of type a.

    The implementation says:

    1. Implementer starts the idNum protocol as the caller. So, she must:

      1. Choose a type a. She quietly makes the same choice as her caller did.
      2. Prove that a is an instance of Num. This is no problem, because she actually knows that a is Fractional, and this implies Num.
      3. Provide a value of type a. Here she chooses valFrac. To be complete, she must then show that valFrac has the type a.
    2. So the implementer now runs the valFrac protocol. She:

      1. Chooses a type 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.
      2. Prove that a is an instance of Fractional. She uses the same proof her caller did.
      3. The implementer of valFrac then promises to provide a value of type a, as needed.

    For completeness, here is the analogous discussion for applyIdFrac. The protocol says:

    1. Caller chooses a type a.
    2. Caller proves that a is Fractional.
    3. Implementer must provide a value of type a.

    The implementation says:

    1. Implementer will execute the idFrac protocol. So, she must:

      1. Choose a type. Here she quietly chooses whatever her caller chose.
      2. Prove that a is Fractional. She passes on her caller's proof of this.
      3. Choose a value of type a. She will execute the valNum protocol to do this; and we must check that this produces a value of type a.
    2. During the execution of the valNum protocol, she:

      1. Chooses a type. Here she chooses the type that idFrac expects, namely a; this also happens to be the type her caller chose.
      2. Prove that 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.
      3. The implementer of 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.

    0 讨论(0)
  • 2020-12-04 00:49

    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
    
    0 讨论(0)
提交回复
热议问题