Spec-Zone.ru › Haskell 9

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

GeneralisedNewtypeDeriving
GeneralizedNewtypeDeriving
Since:

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)

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

  • 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 равна 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 не имеет значения, используется ли стандартный метод или описанный здесь.)

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

© 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.12.1/docs/users_guide/exts/newtype_deriving.html

Spec-Zone.ru

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