-
GeneralisedNewtypeDeriving -
GeneralizedNewtypeDeriving -
- Версия:
-
6.8.1. Британское написание используется начиная с 8.6.1.
- Статус:
Включает хитроумный механизм обобщённого выведения 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)
и реализация использует для Dollars тот же самый словарь Num, что и для 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 #)
Следующий код завершится ошибкой «Illegal unboxed tuple», поскольку выведенный компилятором экземпляр использует синтаксис неупакованных кортежей:
{-# 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. То есть вCне хватает ровно одного аргумента типаj+1.-
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 не имеет значения, используется ли стандартный способ или описанный здесь.)
6.6.5.3. Связанные семейства типов
GeneralizedNewtypeDeriving также работает с некоторыми классами типов, у которых есть связанные семейства типов. Вот пример:
class HasRing a where type Ring a newtype L1Norm a = L1Norm a deriving HasRing
Выведенный экземпляр HasRing будет выглядеть так:
instance HasRing (L1Norm a) where type Ring (L1Norm a) = Ring a
Точнее, если выводимый класс имеет вид
class C c_1 c_2 ... c_m where type T1 t1_1 t1_2 ... t1_n ... type Tk tk_1 tk_2 ... tk_p
а newtype имеет вид
newtype N n_1 n_2 ... n_q = MkN <rep-type>
то можно вывести экземпляр C c_1 c_2 ... c_(m-1) для N n_1 n_2 ... n_q при условии, что:
-
Параметр типа
c_mвстречается ровно один раз в каждой из переменных типа отT1доTk. Представим класс, в котором это условие не выполняется. Например:class Bad a b where type B a instance Bad Int a where type B Int = Char newtype Foo a = Foo a deriving (Bad Int)
Для выведенного экземпляра
Bad IntGHC пришлось бы сгенерировать нечто вроде следующего:instance Bad Int (Foo a) where type B Int = B ???
Теперь мы зашли в тупик: у нас нет способа сослаться на
aв правой части экземпляра семействаB, поэтому такой экземпляр фактически не имеет смысла в контекстеGeneralizedNewtypeDeriving. -
У
Cнет связанных семейств данных (только семейства типов). Чтобы понять, почему семейства данных запрещены, представим следующую ситуацию:class Ex a where data D a instance Ex Int where data D Int = DInt Bool newtype Age = MkAge Int deriving Ex
Для выведенного экземпляра
ExGHC пришлось бы сгенерировать нечто вроде следующего:instance Ex Age where data D Age = ???
Но неясно, что GHC должен подставить вместо
???, поскольку для каждого экземпляра семейства данных необходимо создавать новые конструкторы данных.
Если выполнены оба условия, GHC сгенерирует следующий экземпляр:
instance C c_1 c_2 ... c_(m-1) <rep-type> =>
C c_1 c_2 ... c_(m-1) (N n_1 n_2 ... n_q) where
type T1 t1_1 t1_2 ... (N n_1 n_2 ... n_q) ... t1_n
= T1 t1_1 t1_2 ... <rep-type> ... t1_n
...
type Tk tk_1 tk_2 ... (N n_1 n_2 ... n_q) ... tk_p
= Tk tk_1 tk_2 ... <rep-type> ... tk_p
И снова: если C не содержит методов класса, контекст экземпляра будет избыточным, поэтому GHC вместо этого сгенерирует instance C c_1 c_2 ... c_(m-1) (N n_1 n_2 ... n_q).
Учтите, что в некоторых случаях для использования этой возможности может потребоваться включить расширение UndecidableInstances. Ниже приведён патологический случай, иллюстрирующий, почему это может понадобиться:
class C a where type T a newtype Loop = MkLoop Loop deriving C
В результате будет сгенерирован следующий выведенный экземпляр:
instance C Loop where type T Loop = T Loop
Здесь очевидно, что попытка использовать тип T Loop приведёт к бесконечному циклу проверки типов, поскольку его определение рекурсивно продолжается бесконечно. В других случаях расширение UndecidableInstances может понадобиться даже тогда, когда сгенерированный код не зациклит проверку типов. Например:
instance C Int where type C Int = Int newtype MyInt = MyInt Int deriving C
В результате будет сгенерирован следующий выведенный экземпляр:
instance C MyInt where type T MyInt = T Int
Хотя проверка типов для T MyInt завершится, проверка завершения GHC недостаточно сложна, чтобы это определить, поэтому для использования этого выведенного экземпляра потребуется включить UndecidableInstances. Если вы решите пойти этим путём, убедитесь, что можете доказать себе: все выводимые вами экземпляры семейств типов в конечном итоге завершатся при использовании!
Обратите внимание, что DerivingVia (см. Выведение через) использует практически ту же спецификацию для выведения экземпляров связанных семейств типов (с тем отличием, что вместо базового типа rep-type newtype используется тип via).