Насколько я могу судить, GHC может преобразовать любой числовой литерал с полиморфным типом по умолчанию Num a => a
в любой тип с экземпляром Num
. Я хотел бы знать, правда ли это, и немного о механизме, лежащем в основе этого.
Чтобы изучить это, я написал тип данных под названием MySum
, который частично повторяет функциональность Sum
из Data.Monoid. Самое главное, что он содержит instance Num a => Num (MySum a)
.
Примечание. Так получилось, что мой вопрос начинается с этого. Моноид не имеет особого значения. Я включил часть этого кода в конец этого вопроса на тот случай, если в ответе полезно ссылаться на содержимое.
Кажется, что GHCi с радостью удовлетворит ввод формы «v :: MySum t» при следующих условиях:
v — полиморфное значение типа Num a => a
t является (возможно, полиморфным) типом относительно Num
Насколько я могу судить, единственные числовые литералы, совместимые с типом Num a => a
, — это те, которые выглядят как целые числа. Всегда ли это так? Кажется, подразумевается, что значение может быть создано для любого типа в Num именно тогда, когда это значение является целым. Если это так, то я понимаю, как что-то вроде 5 :: MySum Int
может работать, учитывая функцию fromInteger
в Num
.
С учетом всего сказанного, я не могу понять, как что-то вроде этого работает:
*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
составляет:
fromInteger 5
Поскольку типом fromInteger
является Num a => Integer -> a
, вы можете создать экземпляр 5
для экземпляра Num
по вашему выбору, будь то Int
, Double
, MySum Double
или что-то еще. В частности, учитывая, что Fractional
является подклассом Num
, и что вы написали экземпляр 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
; а литерал с плавающей запятой 5.0
эквивалентен fromRational (5.0 :: Rational)
и имеет тип Fractional a => a
. Это действительно объясняет 5 :: MySum Int
. 5 :: Fractional a => MySum a
не намного сложнее. В соответствии с приведенным выше правилом это расширяется до:
fromInteger (5 :: Integer) :: Fractional a => MySum a
fromInteger
имеет тип Num b => Integer -> b
. Таким образом, чтобы приведенное выше выражение имело тип check, GHC должен объединить b
с MySum a
. Итак, теперь GHC должен решить Num (MySum a)
данное Fractional a
. Num (MySum a)
решается вашим экземпляром, создавая ограничение Num a
. Num
является надклассом Fractional
, поэтому любое решение Fractional a
будет также решением Num a
. Так что все проверяется.
Однако вам может быть интересно, если 5
проходит через fromInteger
здесь, почему значение, которое оказывается внутри MySum
, выглядит как Double
в GHCi? Это связано с тем, что после проверки типов Fractional a => MySum a
по-прежнему остается неоднозначным — когда GHCi начинает печатать это значение, ему нужно фактически выбрать a
, чтобы выбрать соответствующий экземпляр Fractional
, в конце концов. Если бы мы не имели дело с числами, мы могли бы закончить тем, что GHC пожаловался на эту двусмысленность в a
.
Но для этого есть специальный случай в стандарте Haskell. Краткий обзор: если у вас есть проблема неоднозначности, подобная приведенной выше, которая включает только классы числового типа, Haskell в своей мудрости выберет либо Integer
, либо Double
для неоднозначного типа и запустится с первым, который проверяет тип. В данном случае это Double
. Если вы хотите узнать больше об этой функции, этот пост в блоге отлично мотивирует и разъясняет то, что говорит стандарт.
Спасибо - и информация о выборе типа по умолчанию была очень полезной.