-
GeneralisedNewtypeDeriving -
GeneralizedNewtypeDeriving -
Включить хитрый обобщенный механизм производных GHC для
newtype.
Когда вы определяете абстрактный тип с использованием newtype, вы можете захотеть, чтобы новый тип унаследовал некоторые экземпляры от своего представления. В Haskell 98 вы можете унаследовать экземпляры Eq, Ord, Enum и Bounded путем вывода, но для других классов вам нужно написать явное объявление экземпляра. Например, если вы определяете
newtype Dollars = Dollars Int
и хотите использовать арифметические операции над Dollars, вы должны явно определить экземпляр Num:
instance Num Dollars where Dollars a + Dollars b = Dollars (a+b) ...
Всё, что делает экземпляр, — это применяет и удаляет конструктор newtype. Особенно досадно, что поскольку конструктор не появляется во время выполнения, это объявление экземпляра определяет словарь, полностью эквивалентный словарю Int, только медленнее!
DerivingVia (см. Выведение через) является обобщением этой идеи.
6.6.5.1. Обобщение предложения deriving
Сейчас GHC позволяет выводить такие экземпляры вместо этого, используя расширение GeneralizedNewtypeDeriving, поэтому можно написать
newtype Dollars = Dollars { getDollars :: Int } deriving (Eq,Show,Num)
и реализация использует тот же Num словарь для Dollars , что и для Int. Другими словами, GHC сгенерирует нечто подобное следующему коду
instance Num Int => Num Dollars
и затем попытается упростить контекст Num Int как можно сильнее. GHC знает, что в области видимости есть экземпляр Num Int, поэтому он может выполнить ограничение Num Int, оставив код, который фактически генерирует GHC
instance Num Dollars
Можно представить себе, что этот экземпляр реализован с тем же кодом, что и экземпляр Num Int, но с Dollars и getDollars добавлеными по мере необходимости, чтобы обеспечить проверку типов. (На практике GHC использует несколько другой подход к генерации кода. См. раздел Более точное описание ниже для получения более подробной информации.)
Мы также можем вывести экземпляры классов конструкторов аналогичным образом. Например, предположим, что мы реализовали преобразователи монады состояния и ошибки, такие что
instance Monad m => Monad (State s m) instance Monad m => Monad (Failure m)
В Haskell 98 мы можем определить монаду разбора следующим образом
type Parser tok m a = State [tok] (Failure m) a
которая автоматически является монадой благодаря объявлениям экземпляров выше. С расширением мы можем сделать тип парсера абстрактным, не нуждаясь в написании экземпляра класса Monad, через
newtype Parser tok m a = Parser (State [tok] (Failure m) a)
deriving Monad
В этом случае выведенное объявление экземпляра имеет вид
instance Monad (State [tok] (Failure m)) => Monad (Parser tok m)
Обратите внимание, что так как Monad является классом конструкторов, экземпляр является частичным применением newtype, а не всей левой части. Мы можем представить, что объявление типа «преобразуется в эта-форму», чтобы сгенерировать контекст объявления экземпляра.
Мы даже можем вывести экземпляры многопараметрических классов, при условии, что newtype является последним параметром класса. В этом случае в части deriving появляется «частичное применение» класса. Например, данный класс
class StateMonad s m | m -> s where ... instance Monad m => StateMonad s (State s m) where ...
тогда мы можем вывести экземпляр StateMonad для Parser следующим образом
newtype Parser tok m a = Parser (State [tok] (Failure m) a)
deriving (Monad, StateMonad [tok])
Выведенный экземпляр получается путем завершения применения класса к новому типу:
instance StateMonad [tok] (State [tok] (Failure m)) =>
StateMonad [tok] (Parser tok m)
В результате этого расширения все производные экземпляры в объявлениях newtype обрабатываются одинаково (и реализуются просто путем повторного использования словаря для типа представления), кроме Show и Read, которые действительно ведут себя по-разному для newtype и его представления.
Примечание
Иногда необходимо включить дополнительные расширения языка при выводе экземпляров с помощью GeneralizedNewtypeDeriving. Например, рассмотрим простой класс и экземпляр с использованием синтаксиса UnboxedTuples:
{-# LANGUAGE UnboxedTuples #-}
module Lib where
class AClass a where
aMethod :: a -> (# Int, a #)
instance AClass Int where
aMethod x = (# x, x #)
Следующее завершится ошибкой «Незаконная нерас упакованная кортеж», так как производный экземпляр, созданный компилятором, использует синтаксис нерас упакованных кортежей,
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
import Lib
newtype Int' = Int' Int
deriving (AClass)
Однако включение расширения UnboxedTuples позволяет модулю скомпилироваться. Подобные ошибки могут возникать при использовании различных расширений, включая:
6.6.5.2. Более точное описание
Выведенный экземпляр выводится только для объявлений этих форм (после расширения любых синонимов типов)
newtype T v1..vn = MkT (t vk+1..vn) deriving (C t1..tj) newtype instance T s1..sk vk+1..vn = MkT (t vk+1..vn) deriving (C t1..tj)
где
-
v1..vn— это переменные типов, аt,s1..sk,t1..tj— типы. (C t1..tj)— это частичное применение классаC, где арностьCравнаj+1. То есть, уCотсутствует ровно один тип аргумента.-
kвыбирается так, чтобыC t1..tj (T v1...vk)был хорошо типизирован. (Или, в случаеdata instance, чтобыC t1..tj (T s1..sk)был хорошо типизирован.) - Тип
t— это произвольный тип. - Переменные типов
vk+1...vnне встречаются в типахt,s1..sk, илиt1..tj. -
Cне являетсяRead,Show,Typeable, илиData. Эти классы не должны «просматривать» тип или его конструктор. Вы по-прежнему можете выводить эти классы для newtype, но это происходит обычным способом, а не через этот новый механизм. Обратитесь к Стратегии вывода по умолчанию. - Безопасно применить приведение типов к каждому из методов
C. То есть, отсутствующий последний аргументCне используется в номинальном режиме в любом из методовC(см. Роли). -
Cможет иметь связанные семейства типов, при условии, что они удовлетворяют требованиям, изложенным в разделе GND и связанные типы.
Тогда выведенное объявление экземпляра имеет вид
instance C t1..tj t => C t1..tj (T v1...vk)
Обратите внимание, что если C не содержит никаких методов класса, контекст экземпляра совершенно излишен, и в таком случае GHC сгенерирует вместо этого:
instance C t1..tj (T v1..vk)
В качестве примера, который не работает, рассмотрим
newtype NonMonad m s = NonMonad (State s m s) deriving Monad
Здесь мы не можем вывести экземпляр
instance Monad (State s m) => Monad (NonMonad m)
потому что переменная типа s встречается в State s m, и поэтому не может быть «преобразованной в этта-форму». Хорошо, что это объявление deriving отклоняется, потому что NonMonad m фактически не является монадой — по той же причине. Попробуйте определить >>= с правильным типом: у вас не получится.
Обратите также внимание, что порядок параметров класса становится важным, поскольку мы можем выводить экземпляры только для последнего. Если класс StateMonad выше был определен как
class StateMonad m s | m -> s where ...
то мы не смогли бы вывести экземпляр для типа Parser выше. Мы предполагаем, что многопараметрические классы обычно имеют один «главный» параметр, для которого вывод новых экземпляров наиболее интересен.
Наконец, всё это относится только к классам, отличным от Read, Show, Typeable, и Data, для которых применяется стандартный вывод (раздел 4.3.3. отчета Haskell). (Для стандартных классов Eq, Ord, Ix, и Bounded не имеет значения, используется ли стандартный метод или описанный здесь.)