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