Насколько я могу судить, 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
Kevin Bradner 31 Май 2019 в 05:59

2 ответа

Лучший ответ

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

3
user11228628 31 Май 2019 в 04:42

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

4
duplode 31 Май 2019 в 04:06
56388108