2
votes

As far as I can tell, GHC can convert any numeric literal with default polymorphic type Num a => a into any type with an instance of Num. I'd like to know whether this is true and a little about the underlying mechanism.

To explore this, I have written a datatype called MySum which replicates (part of) the functionality of Sum from Data.Monoid. The most important part is that it contains instance Num a => Num (MySum a).

Note - This just so happens to be where my question go its start. The Monoid is not specifically relevant. I've included a portion of that code at the bottom of this question, just in case it is useful for an answer to refer to the contents.

It seems that GHCi will happily comply with an input of the form "v :: MySum t", under the following conditions:

  1. v is a polymorphic value of type Num a => a

  2. t is a (possibly polymorphic) type under Num

As far as I can tell, the only numeric literals compatible with the type Num a => a are ones that look like integers. Is this always the case? It seems to imply that a value can instantiated to any type under Num exactly when that value is integral. If this is correct, then I understand how something like 5 :: MySum Int might work, given the function fromInteger in Num.

With all of that said, I can't figure out how something like this works:

*Main Data.Monoid> 5 :: Fractional a => MySum a
MySum {getMySum = 5.0}

If it's possible to explain this in a novice-friendly way, I would appreciate it.


The instance Num a => Num (MySum a), as promised:

import Control.Applicative

newtype MySum a = MySum {getMySum :: a}
  deriving Show

instance Functor MySum where
  fmap f (MySum a) = MySum (f a)

instance Applicative MySum where
  pure = MySum
  (MySum f) <*> (MySum a) = MySum (f a)

instance Num a => Num (MySum a) where
  (+) = liftA2 (+)
  (-) = liftA2 (-)
  (*) = liftA2 (*)
  negate = fmap negate
  abs = fmap abs
  signum = fmap signum
  fromInteger = pure . fromInteger
2

2 Answers

4
votes

As you have found out, the integer literal 5 amounts to:

fromInteger 5

As the type of fromInteger is Num a => Integer -> a, you can instantiate 5 to the Num instance of your choice, be it Int, Double, MySum Double, or anything else. In particular, given that Fractional is a subclass of Num, and that you wrote a Num a => Num (MySum a) instance, 5 :: Fractional a => MySum a works just fine:

5 :: Fractional a => MySum a
fromInteger 5 :: Fractional a => MySum a
(pure . fromInteger) 5 :: Fractional a => MySum a
MySum (fromInteger 5 :: Fractional a => a)

It seems to imply that a value can instantiated to any type under Num exactly when that value is integral.

Things get a little subtle here. An integral value can be converted to any type under Num (via fromInteger and, in the general case, fromIntegral). We can instantiate an integer literal like 5 as anything under Num because GHC handles the conversion for us, by desugaring it to fromInteger 5 :: Num a => a. However, we can't instantiate the monomorphic value 5 :: Integer as a Double, nor can we instantiate 5 :: Integral a => a to a non-Integral type like Double. In those two cases, the type annotations further restrict the type, so that we have to perform the conversion explicitly if we want a Double.

3
votes

You mostly have it correct: an integer literal 5 is equivalent to fromInteger (5 :: Integer), and thus has type Num a => a; and a floating-point literal 5.0 is equivalent to fromRational (5.0 :: Rational) and has type Fractional a => a. This does indeed explain 5 :: MySum Int. 5 :: Fractional a => MySum a isn't that much trickier. Per the above rule, this expands to:

fromInteger (5 :: Integer) :: Fractional a => MySum a

fromInteger has type Num b => Integer -> b. So for the above expression to type check, GHC has to unify b with MySum a. So now GHC has to solve Num (MySum a) given Fractional a. Num (MySum a) is solved by your instance, producing the constraint Num a. Num is a superclass of Fractional, so any solution to Fractional a will also be a solution to Num a. So everything checks out.

You might be wondering though, if 5 gets passed through fromInteger here, why does the value that ends up inside MySum look like a Double in GHCi? This is because, after type checking, Fractional a => MySum a is still ambiguous—when GHCi goes to print that value, it needs to actually choose an a in order to select an appropriate Fractional instance, after all. If we weren't dealing with numbers, we might end up with GHC complaining about this ambiguity in a.

But there's a special case in the Haskell standard for this. The brief overview is, if you have an ambiguity issue like the above that involves only numeric type classes, Haskell in its wisdom will pick either Integer or Double for the ambiguous type and run with the first one that type checks. In this case, that's Double. If you'd like to know more about this feature, this blog post does a decent job of motivating and elaborating on what the standard says.