Могу ли я автоматически создавать экземпляры классов типов для функции преобразования без чрезмерных разрешений?

Недавно я задал вопрос об экземпляре, который я создал, генерируя бесконечный цикл во время выполнения, и получил замечательный ответ! Теперь, когда я понимаю, что происходит, у меня есть новый вопрос: могу ли я исправить мою попытку достичь своей первоначальной цели?

Позвольте мне повторить и уточнить, в чем конкретно заключается моя проблема: я хочу создать класс типов, который будет использоваться для преобразования между некоторыми эквивалентными типами данных в моем коде. Созданный мной класс типов является очень простым и очень общим: он включает в себя одну функцию преобразования, которая преобразует произвольные типы данных:

class Convert a b where
  convert :: a -> b

Однако этот класс типов имеет конкретную цель: преобразование между определенным классом значений, которые имеют каноническое представление. Поэтому существует определенный тип данных, который является "каноническим", и я хочу использовать свойство этого типа данных, чтобы уменьшить нагрузку на разработчиков моего класса типов.

data Canonical = ...

class ConvertRep a b where
  convertRep :: a -> b

В частности, рассмотрим два разных представления, RepA а также RepB, Я мог бы разумно определить некоторые случаи для преобразования этих представлений в и из Canonical:

instance ConvertRep RepA Canonical where ...
instance ConvertRep Canonical RepA where ...

instance ConvertRep RepB Canonical where ...
instance ConvertRep Canonical RepB where ...

Теперь это сразу полезно, потому что теперь я могу использовать convertRep для обоих видов представлений, но в основном это просто способ перегрузки convertRep название. Я хочу сделать что-то более мощное: в конце концов, я эффективно определил четыре функции следующих типов:

RepA      -> Canonical
Canonical -> RepA
RepB      -> Canonical
Canonical -> RepB

Мне кажется разумным, что, учитывая эти определения, я также должен иметь возможность производить две функции следующих типов:

RepA -> RepB
RepB -> RepA

По сути, поскольку оба типа данных могут быть преобразованы в / из канонического представления, я хочу автоматически создать функцию преобразования непосредственно в / из друг друга. Моя попытка, как упоминалось в моем вышеупомянутом вопросе, выглядела так:

instance (ConvertRep a Canonical, ConvertRep Canonical b) => ConvertRep a b where
  convertRep = convertRep . (convertRep :: a -> Canonical)

К сожалению, этот экземпляр слишком разрешительный и заставляет сгенерированный код повторяться, если он содержит два типа, которые я считаю недопустимыми - типы, для которых я еще не определил канонические преобразования.


Чтобы попытаться решить эту проблему, я рассмотрел другой, более простой подход. Я решил, что мог бы использовать два класса типов вместо одного, чтобы предотвратить эту проблему рекурсии:

class ToCanonical a where
  toCanonical :: a -> Canonical

class FromCanonical a where
  fromCanonical :: Canonical -> a

Теперь можно определить новую функцию, которая выполняет convertRep конверсия меня изначально интересовала:

convertRep :: (ToCanonical a, FromCanonical b) => a -> b
convertRep = fromCanonical . toCanonical

Однако это происходит за счет гибкости: больше невозможно создать экземпляр прямого преобразования между двумя неканоническими представлениями.

Например, возможно, я знаю, что RepA а также RepB будут очень часто использоваться взаимозаменяемо, и, следовательно, они будут преобразованы между собой довольно много. Поэтому дополнительный шаг конвертации в / из Canonical потерянное время. Я хотел бы опционально определить экземпляр прямого преобразования:

instance ConvertRep RepA RepB where
  convertRep = ...

который обеспечивает "быстрый путь" преобразования между двумя распространенными типами.


Подводя итог: есть ли способ достичь всех этих целей, используя систему типов Haskell?

  1. "Генерировать" преобразования между представлениями заданными функциями, которые преобразуют между представлениями и канонической формой.
  2. При желании укажите "быстрые пути" между экземплярами, которые обычно преобразуются.
  3. Отклонить экземпляры, которые не были явно определены; то есть не разрешать ConvertRep Canonical Canonical (и аналогичные) случаи из созданного, которые бесконечно повторяются и производят дно.

Система типов Haskell очень впечатляет, но я беспокоюсь, что в этом случае ее правила разрешения экземпляров не являются достаточно мощными для достижения всех этих целей одновременно.

2 ответа

OverlappingInstances может использоваться для аварийного поведения:

data A = A deriving (Show)
data B = B deriving (Show)
data C = C deriving (Show) -- canonical

class Canonical a where
  toC   :: a -> C
  fromC :: C -> a

class Conv a b where
  to   :: a -> b
  from :: b -> a

instance (Canonical a, Canonical b) => Conv a b where
  to   = fromC . toC
  from = fromC . toC

instance {-# overlapping #-} Conv A B where
  to   _ = B
  from _ = A

instance Canonical A where
  toC _ = C
  fromC _ = A

instance Canonical B where
  toC _ = C
  fromC _ = B

Conv преобразует напрямую, если существует прямой экземпляр, или же возвращается к преобразованию через C, overlapping Прагма сигнализирует, что мы хотим переопределить экземпляр по умолчанию. В качестве альтернативы, мы могли бы поставить overlappable прагма для экземпляра по умолчанию, но это более опасно, поскольку это позволило бы всем остальным экземплярам - возможно, определенным во внешних модулях - иметь возможность переопределения без вывода сообщений.

Однако я не думаю, что эта схема преобразования особенно полезна или полезна. Перекрывающиеся экземпляры создают риск разрешения разных экземпляров в разных модулях, и они могут оказаться в одном и том же модуле через импорт и экзистенциальные экземпляры, что может создать проблемы.

Вы могли бы использовать DefaultSignatures указать реализацию по умолчанию. Хотя это все еще заставляет вас перечислять все действительные преобразования вручную, это относительно компактно, поскольку вы можете положиться на реализацию по умолчанию. То есть что-то вроде

{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ScopedTypeVariables #-}

data Canonical = C

class ConvertRep a b where
  convertRep :: a -> b
  default convertRep :: (ConvertRep a Canonical, ConvertRep Canonical b) => a -> b
  convertRep = convertRep . (convertRep :: a -> Canonical)

data A = A
data B = B

instance ConvertRep A Canonical where
  convertRep A = C

instance ConvertRep Canonical B where
  convertRep C = B

Теперь вы можете определить преобразование между A а также B просто

instance ConvertRep A B

но все еще возможно перезаписать реализацию по умолчанию для каждого типа.

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