使用镜头实现三种或多种类型之间的同态映射

10

有关ADT之间多态函数的问题启发,我正在尝试创建多个(不止两个)类型之间的同构,以便每当我需要同构但不是相同类型时,我可以在我的代码中添加一些convert

假设我有3个ADT:

data AB = A | B deriving (Show)
data CD = C | D deriving (Show)
data EF = E | F deriving (Show)

使用lens,我可以在AB和CD之间以及CD和EF之间实现2个同构:

{-# 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。但如果我想通过AC转换为E,则必须为每个中间转换注释类型:

(C^.from convert :: AB)^.convert :: EF

我理解编译器为什么不能推导出中间类型。可能是因为有几个同构可以从 C 转换到 E。但是,我能简化我的代码,以便不必在每个地方手动注释类型吗?

我可以写一个新实例,直接在 CDEF 之间进行转换,但是如果我有超过3种类型怎么办?如果我有5个同构类型,我将不得不指定10个实例,因为同构对象之间的同构数是完全图中边的数量,这是一个三角形数。我宁愿指定 n-1 个实例,并权衡编写更多的 convertfrom convert

是否有一种惯用的方法来使用来自 lensIso 在多个类型之间建立同构,以便尽量减少样板代码并且我不必在每个地方都加上类型注释?如果我必须使用 TemplateHaskell,我该如何做到这一点?

动机是因为在我的工作中,我有许多非常复杂但很蠢的类型,其中 () -> (() -> ()) -> X((), X) 同构于 X。我必须手动包装和解包所有内容,我想要一种多态的方式将复杂的类型简化为更简单的同构类型。


很遗憾,新的 Data.Coerce 对此并不是很有用。 - jberryman
1个回答

11

你可以将同构结构化为星型图:使用一个规范的“中心”类型,所有其他类型都连接到它。缺点是你必须在每个实例中显式地指定中心,并且只能在共享中心的类型之间进行转换。但是,你的两个要求(良好的类型推断和线性数量的实例)将得到满足。以下是实现方法:

{-# 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)

非常感谢,这太棒了!我有一个问题:如果我想从具体类型X泛化到任何可以被视为“规范”的类型(即(Int, String, Char)是规范的,但(() -> ()) -> (Int, String, Char)不是),该怎么办?我不仅使用X,还使用任意类型,包括任意元组。 - Mirzhan Irkegulov
@MirzhanIrkegulov 感谢你帮我发现了这个 bug。=)对于您考虑为规范的所有类型,您将需要基本情况。无论您选择什么设计,我真的认为没有绕过这种方式的方法。 - Daniel Wagner
花了几个小时后,我仍然不清楚如何将同构 ((),()) ≅ () 集成到上述方案中。我在某种程度上遇到了冲突的家族实例。 - Mirzhan Irkegulov
@MirzhanIrkegulov 这有点不幸,不过需要注意箭头的情况和元组的情况之间的区别。在箭头的情况下,我们可以决定箭头的一侧必然是我们计划要丢弃的单元,因此我们可以使用多态并将 Unit a 添加到上下文中。而在元组的情况下,每一侧都可能是单元,所以我定义的这两个实例必须是单态的。我不确定如何处理这个问题,我能想到的最好的方法需要进行大量的类型级别计算,这对于如此微小的问题来说会是一个重磅炸弹。 - Daniel Wagner

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接