我对isomorphism的顶级答案有一些异议,因为范畴论对isomorphism的定义并没有涉及到对象。为了理解这点,让我们回顾一下它的定义。
定义
isomorphism是一对态射(即函数)f
和g
,满足以下条件:
f . g = id
g . f = id
这些态射被称为“同构” 。很多人没有意识到同构中的“态射”指的是函数而不是对象。但是,你会说它们连接的对象是“同构”的,这就是另一个回答所描述的内容。
请注意,同构的定义并没有说明(
.
),
id
, 或
=
必须是什么。唯一的要求是,无论它们是什么,它们也必须满足范畴定律:
f . id = f
id . f = f
(f . g) . h = f . (g . h)
Composition(即( .
))将两个态射合并为一个态射,而 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编码
同构的一个用途是将数据类型编码为函数,这称为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编码是延续传递风格),并且可以在根本没有对代数数据类型提供语言支持的语言中实现。
翻译实现
有时候人们会尝试比较一个库的某个特性的实现与另一个库的实现,如果你能证明它们是同构的,那么就可以证明它们同样强大。此外,同构描述了如何将一个库转换为另一个库。
例如,有两种方法可以从functor的签名定义monad。一种是由free
包提供的自由monad,另一种是由operational
包提供的操作语义。
如果您查看这两个核心数据类型,它们看起来不同,尤其是它们的第二个构造器:
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
类型(来自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
这意味着,如果我们组合这两个镜头,结果应该与恒等镜头无法区分。
iso-
表示相等/等价:http://en.wiktionary.org/wiki/iso-#Etymology,而-morph-
表示形状/形式:http://www.thefreedictionary.com/morph。因此,同构(isomorphism)是指两种类型之间的等价关系/函数,如何定义已经在下面非常好地解释了。 - Cetin Sert