我对同构的赞成答案提出了一点问题,因为同构的范畴论定义对对象没有任何说明。要了解原因,让我们回顾一下定义。
定义
同构是一对态射(即函数)f
和g
,这样:
f . g = id
g . f = id
这些态射然后被称为“iso”态射。很多人没有注意到同构中的“态射”是指函数而不是对象。但是,您会说它们连接的对象是“同构的”,这就是另一个答案所描述的。
请注意,同构的定义并没有说明 ( .
) id
、 或=
必须是什么。唯一的要求是,无论它们是什么,它们也满足类别法则:
f . id = f
id . f = f
(f . g) . h = f . (g . h)
组合(即 ( .
))将两个态射连接成一个态射,并id
表示某种“同一性”转换。这意味着如果我们的同构抵消了恒等态射id
,那么您可以将它们视为彼此的逆。
对于态射是函数的特定情况,则id
定义为恒等函数:
id x = x
...和组成被定义为:
(f . g) x = f (g x)
...如果两个函数在组合它们时抵消了恒等函数,则它们是同构的id
。
态射与对象
但是,有多种方式可以使两个对象同构。例如,给定以下两种类型:
data T1 = A | B
data T2 = C | D
它们之间有两个同构:
f1 t1 = case t1 of
A -> C
B -> D
g1 t2 = case t2 of
C -> A
D -> B
(f1 . g1) t2 = case t2 of
C -> C
D -> D
(f1 . g1) t2 = t2
f1 . g1 = id :: T2 -> T2
(g1 . f1) t1 = case t1 of
A -> A
B -> B
(g1 . f1) t1 = t1
g1 . f1 = id :: T1 -> T1
f2 t1 = case t1 of
A -> D
B -> C
g2 t2 = case t2 of
C -> B
D -> A
f2 . g2 = id :: T2 -> T2
g2 . f2 = id :: T1 -> T1
所以这就是为什么最好根据与两个对象相关的特定函数而不是两个对象来描述同构,因为在两个对象之间可能不一定存在满足同构定律的唯一函数对。
另外,请注意,仅使函数可逆是不够的。例如,以下函数对不是同构:
f1 . g2 :: T2 -> T2
f2 . g1 :: T2 -> T2
即使您撰写时没有丢失任何信息f1 . g2
,您也不会返回到您的原始状态,即使最终状态具有相同的类型。
此外,同构不必在具体数据类型之间。这是两个规范同构的示例,它们不在具体的代数数据类型之间,而是简单地关联函数:curry
和uncurry
:
curry . uncurry = id :: (a -> b -> c) -> (a -> b -> c)
uncurry . curry = id :: ((a, b) -> c) -> ((a, b) -> c)
同构的用途
教会编码
同构的一种用途是将数据类型 Church 编码为函数。例如,Bool
同构于forall a . a -> a -> a
:
f :: Bool -> (forall a . a -> a -> a)
f True = \a b -> a
f False = \a b -> b
g :: (forall a . a -> a -> a) -> Bool
g b = b True False
验证f . g = id
和g . f = id
。
Church 编码数据类型的好处是它们有时运行得更快(因为 Church-encoding 是连续传递样式),并且它们可以用甚至根本不支持代数数据类型的语言来实现。
翻译实现
有时有人试图将一个库的某些功能实现与另一个库的实现进行比较,如果你能证明它们是同构的,那么你就可以证明它们同样强大。此外,同构描述了如何将一个库转换为另一个库。
例如,有两种方法可以提供从函子的签名定义单子的能力。一个是免费的 monad,由free
包提供,另一个是操作语义,由operational
包提供。
如果你看一下这两种核心数据类型,它们看起来是不同的,尤其是它们的第二个构造函数:
-- modified from the original to not be a monad transformer
data Program instr a where
Lift :: a -> Program instr a
Bind :: Program instr b -> (b -> Program instr a) -> Program instr a
Instr :: instr a -> Program instr a
data Free f r = Pure r | Free (f (Free f r))
...但它们实际上是同构的!这意味着这两种方法同样强大,并且用一种方法编写的任何代码都可以使用同构机械地翻译成另一种方法。
不是函数的同构
此外,同构不限于函数。它们实际上是为任何定义的Category
,Haskell 有很多类别。这就是为什么考虑态射而不是数据类型更有用的原因。
例如,Lens
类型 (from data-lens
) 构成一个类别,您可以在其中组合镜头并拥有一个身份镜头。所以使用我们上面的数据类型,我们可以定义两个同构的透镜:
lens1 = iso f1 g1 :: Lens T1 T2
lens2 = iso g1 f1 :: Lens T2 T1
lens1 . lens2 = id :: Lens T1 T1
lens2 . lens1 = id :: Lens T2 T2
请注意,有两个同构在起作用。一个是用于构建每个透镜的同构(即f1
和g1
)(这也是构造函数被称为 的原因iso
),然后透镜本身也是同构。请注意,在上面的公式中,使用的组合 ( .
) 不是函数组合,而是透镜组合,并且id
不是恒等函数,而是恒等透镜:
id = iso id id
这意味着如果我们组合两个镜头,结果应该与那个身份镜头没有区别。