同构函数的重要性


74

简短的问题:同构函数在编程中(即在函数编程中)的重要性是什么?

漫长的问题:我试图根据我不时听到的一些术语在功能编程和类别理论中的概念之间画出一些相似的地方。本质上,我试图将这些术语“解包”为具体内容,然后再进行扩展。这样,我就可以在了解我所谈论的到底是什么的情况下使用该术语。总是很好。

我一直听到的其中一个术语是同构,我认为这是关于功能或功能组成之间的对等的推理。我想知道是否有人可以提供一些常见的模式的见解,在这些模式中,同构的特性派上用场(在函数式编程中),以及获得的任何副产品,例如通过对同构函数进行推理而进行的编译器优化。


这是一个很好的问题。您要创建一系列吗?如果是这样,则应在某处收集链接,并在个人资料中链接到该收集。
Marcin 2012年

2
@Marcin我的确是,当我全部编译
完后

3
不仅是一个好问题,而且还很好地使用了StackOverflow!
约尔格W¯¯米塔格

iso-表示均等(/等价):en.wiktionary.org/wiki/iso-#Etymology-morph-表示形状/形式:thefreedictionary.com/morph。因此,同构是两种类型之间的等价关系/函数,如何定义一个同构实际上已经在下面很好地解释了。
Cetin Sert,2012年

Answers:


81

我对同构的答案提出了一个小问题,因为同构的范畴理论定义对对象没有任何影响。要了解原因,让我们回顾一下定义。

定义

同构是一对同构(即函数)fg,例如:

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,即使最终状态具有相同的类型,您也不会返回到原始状态。

同样,同构也不必在具体的数据类型之间。这是两个典型同构不在具体代数数据类型之间的示例,而只是简单地将函数联系起来:curryuncurry

curry . uncurry = id :: (a -> b -> c) -> (a -> b -> c)
uncurry . curry = id :: ((a, b) -> c) -> ((a, b) -> c)

同构的用途

教会编码

同构的一种用途是对数据​​类型进行教堂编码。例如,Boolforall 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 = idg . f = id

Church编码数据类型的好处是它们有时运行速度更快(因为Church-encoding是连续传递样式),并且可以用根本不支持代数数据类型的语言来实现它们。

翻译实施

有时,人们试图将一个库的某些功能的实现与另一库的实现进行比较,如果可以证明它们是同构的,则可以证明它们具有同等的功能。同样,同构描述了如何将一个库转换为另一个库。

例如,有两种方法可以根据函子的签名定义单子。一个是free软件包提供的免费monad,另一个是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

请注意,有两个同构在起作用。一个是用于构建每个透镜(即f1g1)的同构(这也是为什么将构建函数称为iso)的原因,然后透镜本身也是同构的。请注意,在以上公式中,所.使用的组合物()不是功能组合物,而是镜片组合物,而id不是身份函数,而是身份镜片:

id = iso id id

这意味着,如果我们将两个镜头组合在一起,结果应该与那个身份镜头没有区别。


这将有助于显示之间的同构ProgramFree; 我无法一目了然。
散热器

Program fFree (Coyoneda f)Coyoneda f同构f,这就是为什么两种类型同构的原因。我把它省略了,因为证明涉及更多。
加布里埃尔·冈萨雷斯

我希望我了解Program的情况。这是一个有效的定义吗?instr的作用是什么?我猜想这可能与混淆输入错误的编辑有关,我很高兴看到它,但还不太清楚该如何做。
养猪工人

@pigworker我从中复制粘贴了该内容operational,但删除了基本monad,使其成为普通monad而不是monad转换器。在此过程中,我忘了T从构造函数中删除,但已将其修复。
加布里埃尔·冈萨雷斯

@GabrielGonzalez instr的作用是什么?
养猪工人

25

一个同构 u :: a -> b是具有一个功能,即,另一种功能v :: b -> a ,以使关系

u . v = id
v . u = id

很满意。您说如果两个类型之间存在同构,则它们是同构的。从本质上讲,这意味着您可以将它们视为同一类型-您可以对一个进行任何处理,而对另一个进行处理。

功能同构

两种功能类型

(a,b) -> c
a -> b -> c

是同构的,因为我们可以写

u :: ((a,b) -> c) -> a -> b -> c
u f = \x y -> f (x,y)

v :: (a -> b -> c) -> (a,b) -> c
v g = \(x,y) -> g x y

您可以检查u . vv . u都是id。实际上,功能uv通过名称curry和更好地了解uncurry

同构和新类型

每当我们使用新类型声明时,我们就利用同构。例如,状态monad的基础类型s -> (a,s)可能有点令人困惑。通过使用新类型声明:

newtype State s a = State { runState :: s -> (a,s) }

我们生成了一个与之State s a同构的新类型,s -> (a,s)并且在使用时将其弄清楚了,我们正在考虑具有可修改状态的函数。我们还为新类型提供了一个方便的构造方法State和一个吸气剂runState

Monads和Comonads

对于更高级的观点,请考虑上面curryuncurry我使用的同构。该Reader r a类型具有newtype声明

newType Reader r a = Reader { runReader :: r -> a }

因此,在单子的上下文中,f产生阅读器的函数具有类型签名

f :: a -> Reader r b

相当于

f :: a -> r -> b

这是咖喱/无咖喱同形的一半。我们还可以定义CoReader r a类型:

newtype CoReader r a = CoReader { runCoReader :: (a,r) }

可以做成一个comonad。在那里,我们有一个函数cobind,或者=>>它带有一个接受coreader并产生原始类型的函数:

g :: CoReader r a -> b

同构

g :: (a,r) -> b

但是我们已经看到了a -> r -> b并且(a,r) -> b是同构的,这给了我们一个不平凡的事实:阅读器monad(具有monadic绑定)和coreader comonad(具有comonadic cobind)也是同构的!特别是,它们都可以用于相同的目的-提供一个遍历每个函数调用的全局环境。


嗯?我忘记了合成运算符'。'..又是什么?串联吗?.. noo

13

考虑数据类型。例如,在Haskell中,如果存在一对以独特方式在它们之间转换数据的函数,则可以认为两种数据类型是同构的。以下三种类型是同构的:

data Type1 a = Ax | Ay a
data Type2 a = Blah a | Blubb
data Maybe a = Just a | Nothing

您可以将在它们之间转换的功能视为同构。这符合同构的分类思想。如果在Type1Type2之间存在两个函数fgwith f . g = g . f = id,则这两个函数是这两种类型(对象)之间的同构。


5
补充一点:f和等功能的技术术语g是双射。
休恩

2
这样一来,map您还可以在不同类型的列表之间建立同构。fmap和适用类型相同。
Riccardo T.

2
那么,如果数据类型之间存在双射关系,它们是同构的吗?
Marcin 2012年

@ertes所以在以下情况下的id是xfunc = f . g...func x
ThaDon 2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.