Изоморфизм между 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)