GHC 如何使用 Num 实例将多态数字文字转换为任意类型?

How can GHC convert a polymorphic numeric literal into an arbitrary type with an instance of Num?

据我所知,GHC 可以将任何具有默认多态类型 Num a => a 的数字文字转换为具有实例 Num 的任何类型。我想知道这是否属实,并了解一些基本机制。

为了探索这一点,我编写了一个名为 MySum 的数据类型,它从 Data.Monoid 复制了 Sum 的(部分)功能。最重要的部分是它包含 instance Num a => Num (MySum a).

注意 - 这恰好是我的问题开始的地方。 Monoid 不是特别相关。我在这个问题的底部包含了该代码的一部分,以防万一它对参考内容的答案很有用。

在以下条件下,GHCi 似乎很乐意接受 "v :: MySum t" 形式的输入:

  1. v 是 Num a => a

  2. 类型的多态值
  3. t 是 Num

  4. 下的一个(可能是多态的)类型

据我所知,与 Num a => a 类型兼容的唯一数字文字是那些看起来像整数的文字。总是这样吗?这似乎意味着当该值是整数时,该值可以实例化为 Num 下的任何类型。如果这是正确的,那么我明白 5 :: MySum Int 这样的东西是如何工作的,给定 Num.

中的函数 fromInteger

综上所述,我无法弄清楚这样的东西是如何工作的:

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

如果可以用新手友好的方式解释这一点,我将不胜感激。


实例Num a => Num (MySum a),如约:

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

如您所见,整数文字 5 amounts to:

fromInteger 5

由于fromInteger的类型是Num a => Integer -> a,您可以将5实例化为您选择的Num实例,无论是IntDoubleMySum Double 或其他任何内容。特别是,鉴于 FractionalNum 的子类,并且您编写了一个 Num a => Num (MySum a) 实例,5 :: Fractional a => MySum a 工作得很好:

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.

事情在这里变得有点微妙。整数值可以转换Num下的任何类型(通过fromInteger,在一般情况下,fromIntegral)。我们可以将像 5 这样的整数文字实例化为 Num 下的任何内容,因为 GHC 通过将其脱糖为 fromInteger 5 :: Num a => a 来为我们处理转换。但是,我们不能将单态值 5 :: Integer 实例化为 Double,也不能将 5 :: Integral a => a 实例化为非 Integral 类型,如 Double。在这两种情况下,类型注释进一步限制了类型,因此如果我们想要 Double,我们必须显式地执行转换。

你基本上是正确的:整数文字 5 等价于 fromInteger (5 :: Integer),因此类型为 Num a => a; floating-point 文字 5.0 等价于 fromRational (5.0 :: Rational) 并且类型为 Fractional a => a。这确实解释了 5 :: MySum Int5 :: Fractional a => MySum a 并没有那么棘手。根据上述规则,这扩展为:

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

fromInteger 的类型为 Num b => Integer -> b。所以对于上面的表达式类型检查,GHC 必须统一 bMySum a。所以现在 GHC 必须解决给定 Fractional aNum (MySum a)Num (MySum a) 由您的实例求解,产生约束 Num aNumFractional 的超类,因此 Fractional a 的任何解决方案也将是 Num a 的解决方案。所以一切都检查好了。

你可能想知道,如果 5 在这里通过 fromInteger,为什么最终在 MySum 中的值看起来像 GHCi 中的 Double ?这是因为,在类型检查之后,Fractional a => MySum a 仍然不明确——当 GHCi 打印那个值时,它需要实际选择一个 a 以便 select 一个合适的 Fractional 毕竟实例。如果我们不处理数字,我们可能会以 GHC 抱怨 a.

中的这种歧义而告终

但有一个特例 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 在激励和详细说明标准内容方面做得不错。