Как GHC может преобразовать полиморфный числовой литерал в произвольный тип с экземпляром Num?

Насколько я могу судить, 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
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
0
100
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

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

Спасибо - и информация о выборе типа по умолчанию была очень полезной.

Kevin Bradner 01.06.2019 03:15

Другие вопросы по теме