Могу ли я автоматически создавать экземпляры классов типов для функции преобразования без чрезмерных разрешений?
Недавно я задал вопрос об экземпляре, который я создал, генерируя бесконечный цикл во время выполнения, и получил замечательный ответ! Теперь, когда я понимаю, что происходит, у меня есть новый вопрос: могу ли я исправить мою попытку достичь своей первоначальной цели?
Позвольте мне повторить и уточнить, в чем конкретно заключается моя проблема: я хочу создать класс типов, который будет использоваться для преобразования между некоторыми эквивалентными типами данных в моем коде. Созданный мной класс типов является очень простым и очень общим: он включает в себя одну функцию преобразования, которая преобразует произвольные типы данных:
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?
- "Генерировать" преобразования между представлениями заданными функциями, которые преобразуют между представлениями и канонической формой.
- При желании укажите "быстрые пути" между экземплярами, которые обычно преобразуются.
- Отклонить экземпляры, которые не были явно определены; то есть не разрешать
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
но все еще возможно перезаписать реализацию по умолчанию для каждого типа.