Как GHC может преобразовать полиморфный числовой литерал в произвольный тип с экземпляром Num? - PullRequest
2 голосов
/ 31 мая 2019

Насколько я могу судить, GHC может преобразовать любой числовой литерал с полиморфным типом по умолчанию Num a => a в любой тип с экземпляром Num. Я хотел бы знать, правда ли это, и немного о базовом механизме.

Чтобы исследовать это, я написал тип данных с именем MySum, который копирует (часть) функциональность Sum из Data.Monoid. Наиболее важной частью является то, что он содержит instance Num a => Num (MySum a).

Примечание. Так случилось, что мой вопрос начался. Моноид конкретно не актуален. Я включил часть этого кода в конец этого вопроса, на случай, если для ответа будет полезно обратиться к содержанию.

Похоже, что GHCi с радостью выполнит ввод в форме "v :: MySum t" при следующих условиях:

  1. v - это полиморфное значение типа Num a => a

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

Ответы [ 2 ]

4 голосов
/ 31 мая 2019

Как вы узнали, целочисленный литерал 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)

Кажется, подразумевается, что значение может быть создано для любого типа в Num именно тогда, когда это значение является целым.

Здесь все становится немного тоньше. Интегральное значение может быть преобразовано в любой тип в Num (через fromInteger и, в общем случае, fromIntegral). Мы можем создать целочисленный литерал, такой как 5, как что-либо в Num, потому что GHC обрабатывает преобразование для нас, обнуляя его до fromInteger 5 :: Num a => a. Однако мы не можем создать экземпляр мономорфного значения 5 :: Integer как Double и не можем создать экземпляр 5 :: Integral a => a для типа, отличного от Integral, например Double. В этих двух случаях аннотации типов еще более ограничивают тип, поэтому мы должны выполнить преобразование явно, если мы хотим Double.

3 голосов
/ 31 мая 2019

В большинстве случаев это правильно: целочисленный литерал 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. Таким образом, для проверки вышеупомянутого выражения 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. Если вы хотите узнать больше об этой функции, это сообщение в блоге делает хорошую работу по мотивации и разработке того, что говорит стандарт.

...