Изоморфизм между 3 и более типами с использованием объектива
Вдохновленный вопрос о полиморфной функции между ADT. Я пытаюсь создать изоморфизмы между несколькими (не только 2) типами, так что каждый раз, когда мне нужен изоморфный но не тот же тип, я могу посыпать свой код некоторым convert
.
Предположим, что у меня есть 3 ADT:
data AB = A | B deriving (Show)
data CD = C | D deriving (Show)
data EF = E | F deriving (Show)
Используя lens
Я могу реализовать 2 изоморфизма между AB и CD, а CD и EF:
{-# LANGUAGE MultiParamTypeClasses #-}
class Isomorphic a b where
convert :: Iso' a b
instance Isomorphic AB CD where
convert = iso ab2cd cd2ab
where ab2cd A = C
ab2cd B = D
cd2ab C = A
cd2ab D = B
instance Isomorphic AB EF where
convert = iso ab2ef ef2ab
where ab2ef A = E
ab2ef B = F
ef2ab E = A
ef2ab F = B
Преобразование A
в E
легко: A^.convert :: EF
. Преобразование D
в B
также легко: D^.from convert :: AB
. Но если я хочу преобразовать из C
в E
через A
, я должен аннотировать типы для каждого промежуточного преобразования:
(C^.from convert :: AB)^.convert :: EF
Я понимаю, почему компилятор не может вывести промежуточные типы. Может быть, существует несколько изоморфизмов, через которые можно получить от C
до E
. Но могу ли я упростить свой код, поэтому я не вручную аннотирую типы везде?
Я мог бы просто написать еще один экземпляр для прямого преобразования между CD
и EF
, но что, если у меня есть более 3 типов? Если бы у меня было 5 изоморфных типов, мне пришлось бы указать 10 экземпляров, потому что число изосов между изоморфными объектами - это число ребер в полном графике, которое является треугольное число. Я бы предпочел указать экземпляры n-1
, с компромиссом, который я пишу больше convert
или from convert
.
Существует ли идиоматический способ установить изоморфизмы между несколькими типами, используя Iso
от lens
, чтобы было минимальное количество шаблонов и мне не нужно вводить все-аннотировать все? Если мне нужно использовать TemplateHaskell для этого, как мне это сделать?
Мотивация заключается в том, что в моей работе у меня много смехотворно сложных, но глупых типов, где () -> (() -> ()) -> X
и ((), X)
изоморфны X
. Я должен вручную обернуть и развернуть все, и я бы хотел, чтобы какой-то полиморфный способ уменьшить сложные типы до более простых изоморфных типов.
Ответы
Ответ 1
Вы можете структурировать свои изоморфизмы как звездный график: иметь канонический тип "хаба", к которому все остальные подключаются. Недостатком является то, что вам нужно будет указать концентратор явно в каждом экземпляре, и вы сможете конвертировать только между типами, которые совместно используют концентратор. Однако ваши два требования (хороший тип вывода и линейное число экземпляров) будут выполнены. Вот как вы это сделаете:
{-# LANGUAGE TypeFamilies #-}
import Control.Lens
import Unsafe.Coerce
data AB = A | B deriving (Show)
data CD = C | D deriving (Show)
data EF = E | F deriving (Show)
class Isomorphic a where
type Hub a
convert :: Iso' a (Hub a)
viaHub :: (Isomorphic a, Isomorphic b, Hub a ~ Hub b) => a -> b
viaHub x = x ^. convert . from convert
instance Isomorphic AB where
type Hub AB = AB
convert = id
instance Isomorphic CD where
type Hub CD = AB
convert = unsafeCoerce -- because I'm too lazy to do it right
instance Isomorphic EF where
type Hub EF = AB
convert = unsafeCoerce
В ghci:
> viaHub A :: EF
E
> viaHub A :: CD
C
> viaHub E :: AB
A
> viaHub E :: CD
C
Здесь вы можете использовать это для своих примеров:
class Unit a where unit :: a
instance Unit () where unit = ()
instance Unit b => Unit (a -> b) where unit _ = unit
instance Isomorphic X where
type Hub X = X
convert = id
instance (Unit a, Isomorphic b) => Isomorphic (a -> b) where
type Hub (a -> b) = Hub b
convert = iso ($unit) const . convert
instance Isomorphic a => Isomorphic ((), a) where
type Hub ((), a) = Hub a
convert = iso snd ((,)()) . convert
instance Isomorphic a => Isomorphic (a, ()) where
type Hub (a, ()) = Hub a
convert = iso fst (flip(,)()) . convert
Теперь у вас будет, например,
viaHub :: (() -> (() -> ()) -> X) -> ((), X)