Spec-Zone.ru › Haskell 9

6.6.5. Обобщённое выведение экземпляров для newtype

GeneralisedNewtypeDeriving
GeneralizedNewtypeDeriving
Версия:

6.8.1. Британское написание используется начиная с 8.6.1.

Статус:

Включено в GHC2024, GHC2021

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

  • UnboxedTuples
  • PolyKinds
  • MultiParamTypeClasses
  • FlexibleContexts

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 Int GHC пришлось бы сгенерировать нечто вроде следующего:

    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
    

    Для выведенного экземпляра Ex GHC пришлось бы сгенерировать нечто вроде следующего:

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

© 2002–2007 The University Court of the University of Glasgow. All rights reserved.
Licensed under the Glasgow Haskell Compiler License.
https://downloads.haskell.org/~ghc/9.14.1/docs/users_guide/exts/newtype_deriving.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API