Why can a Num act like a Fractional?
Asked Answered
S

4

4

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 Nums that are not Fractionals.

So why can I pass a Num argument into a Fractional parameter? Can you explain?

Slinkman answered 15/3, 2017 at 21:1 Comment(2)
Possibly related question that you might find interesting: given that undefined :: a, why does undefined :: Num a => a work? There are certainly as that are not Nums.Twoply
One can see that 'every Fractional is also a Num' by running: Prelude> :info Fractional which results in: class Num a => Fractional a where […omitted…].Breedlove
T
7

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.

Twoply answered 15/3, 2017 at 22:6 Comment(2)
Sorry for the late reply. This is a lot to digest. I think your answer is the key to understanding all this. Can you explain where forall a . comes from all of a sudden? You just added it there. In particular, can there be anything other than forall a . there?Slinkman
@Slinkman Right, bare Haskell implicitly universally quantifies any free type variables -- that is, it implicitly puts forall a b c. ... in front of every type, mentioning all the variables in the type in the forall. While Haskell itself doesn't allow any other quantifiers in that position, there are languages that have a dual exists quantifier; and also forall's can go in other places with some extensions, as in Int -> (forall a. a -> a) -> Int or similar.Twoply
J
6

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
Julie answered 15/3, 2017 at 21:7 Comment(2)
So the types of foo and bar unify and we can share the typeclass dictionary?Underworld
@Underworld Yes, the types of foo and bar unify. No, GHC can't share the typeclass dictionary: it must supply a Num dictionary to one and a Fractional dictionary to the other. But the Fractional dictionary contains a Num dictionary, and that part at least is shared.Twoply
B
2

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.

Blanc answered 15/3, 2017 at 22:52 Comment(0)
X
0

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.)

Xanthus answered 30/9, 2021 at 13:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.