haskelltypessubclassclass-hierarchyadhoc-polymorphism

Why Num can be treated as Floating in haskell?


I defined a function that computes sqrt and converts argument from and to Integral class:

isqrt :: Integral i => i -> i
isqrt = floor . sqrt . fromIntegral

I don't understand why that compiles. If we write out signatures of the individual functions we get:

fromIntegral :: (Num b, Integral a) => a -> b
sqrt :: Floating a => a -> a
floor :: (RealFrac a, Integral b) => a -> b

So "sqrt" takes something of Floating type but it is supplied with Num. If you take a look at class hierarchy, you can see that Floating "inherits" from Num but not the other way around. I would understand if Floating could be implicitly treated as Num because it is a more "specialized" type. Why this is OK for the compiler ?


Solution

  • No, sqrt is not supplied with any Num.

    Its supplier fromIntegral is able to produce any Num as needed indeed, whereas sqrt demands a Floating as its input. And Floating is a subclass of Num.

    So fromIntegral happily obliges. Since it can produce any Num, surely it can produce any type in any subclass of Num. So whatever the specific concrete type it ends up to be, it is in Floating, and thus necessarily it is in Num.

    Thus fromIntegral has no problem providing it.

    edit: Floating is not a type. It is a class of types. A concrete type might be in Floating class of types. Since Floating is a subclass of Num, such type is also guaranteed to be in Num. It is not something that happens automatically due to "inheritance" that you mention. That "inheritance" i.e. subclass relation is a requirement on that specific type which is Floating to also be (in) Num i.e. to implement Num's methods as well as the ones from Floating.

    So yes, a (specific, concrete) Floating type can also be treated as a Num type.

    On the other side of things, sqrt produces the same type as its input type, so it will be the same concrete Floating type. But floor expects a RealFrac type.

    Observe

    > :t floor            ----------
    floor :: (Integral b, RealFrac a) => a -> b
    
    > :t sqrt . fromIntegral            ----------
    sqrt . fromIntegral :: (Integral a, Floating c) => a -> c
    
    > :t floor . sqrt            ----------  ----------
    floor . sqrt :: (Integral c, RealFrac b, Floating b) => b -> c
    
    > :i Floating
    class Fractional a => Floating a where
      ....
    instance Floating Float
    instance Floating Double
    
    > :i RealFrac
    class (Real a, Fractional a) => RealFrac a where
      ....
    instance RealFrac Float
    instance RealFrac Double
    

    So the concrete type produced (and thus, accepted) by that sqrt call must be (in) RealFrac as well as Floating.

    Since it is not observed from outside of that application chain it could be any compliant type, and is thus ambiguous; but type defaulting kicks in and selects Double, unless you've changed the defaults.