Skip to main content

第 10 章:自然变换(Natural Transformations)

原文:Bartosz Milewski, Category Theory for Programmers, Scala Edition, Chapter 10. 原书 PDF、.tex 源文件及相关图像采用 CC BY-SA 4.0;本译文按同一许可发布。

我们已经把函子讨论为保持结构的范畴间映射。

函子把一个范畴“嵌入”另一个范畴。它可以把多个东西折叠成一个,但从不会破坏连接。理解它的一种方式是:借助函子,我们在一个范畴内部为另一个范畴建模。源范畴作为模型,或者说蓝图,描述目标范畴中某个结构的一部分。

同一个源范畴可以以多种方式嵌入目标范畴

把一个范畴嵌入另一个范畴可能有很多方式。有时它们等价,有时差异很大。一个嵌入可能把整个源范畴折叠成一个对象,另一个可能把每个对象映射到不同对象,把每个态射映射到不同态射。同一份蓝图可以有许多不同实现。自然变换帮助我们比较这些实现。它们是函子的映射,而且是保持函子性质的特殊映射。

考虑范畴 $C$ 与 $D$ 之间的两个函子 $F$ 和 $G$。如果只关注 $C$ 中一个对象 a,它会被映射到两个对象:F aG a。因此,函子的映射应该把 F a 映射到 G a

自然变换的分量连接 F a 与 G a

注意,F aG a 是同一个范畴 $D$ 中的对象。同一范畴中对象之间的映射,不应该违背这个范畴本身的纹理。我们不想在人为对象之间制造连接。所以很自然地,我们使用已有连接,也就是态射。自然变换是一组态射的选择:对每个对象 a,它选出一个从 F aG a 的态射。如果把自然变换称为 alpha,这个态射就称为 alphaa 处的分量,也就是 alpha_a

alpha_a :: F a -> G a

记住,a 是 $C$ 中的对象,而 alpha_a 是 $D$ 中的态射。如果对某个 a,在 $D$ 中不存在从 F aG a 的态射,那么 $F$ 与 $G$ 之间就不可能存在自然变换。

当然,这只讲了一半,因为函子不仅映射对象,也映射态射。那么自然变换如何处理这些映射?事实证明,态射映射是固定的:在 $F$ 与 $G$ 之间的任意自然变换下,F f 必须被转换为 G f。而且,两个函子对态射的映射会极大限制我们定义与之兼容的自然变换时的选择。

考虑 $C$ 中两个对象 ab 之间的态射 f。它被映射为 $D$ 中的两个态射 F fG f

F f :: F a -> F b
G f :: G a -> G b

自然变换 alpha 提供两个额外态射,在 $D$ 中补完整个图:

alpha_a :: F a -> G a
alpha_b :: F b -> G b

自然性方块

现在,从 F aG b 有两条路径。为了确保它们相等,必须施加对任意 f 都成立的自然性条件:

G f . alpha_a = alpha_b . F f

自然性条件是相当严格的要求。例如,如果态射 F f 可逆,那么自然性就会用 alpha_a 决定 alpha_b。它沿着 f 传输 alpha_a

alpha_b = (G f) . alpha_a . (F f)^-1

沿 f 传输自然变换分量

如果两个对象之间有多个可逆态射,那么所有这些传输必须一致。一般来说,态射并不可逆;但可以看到,两个函子之间自然变换的存在远非理所当然。因此,与自然变换相关联的函子是稀少还是丰富,能告诉你很多关于这些函子所连接的范畴结构的信息。讨论极限和 Yoneda 引理时,我们会看到一些例子。

从分量角度看自然变换,可以说它把对象映射到态射。由于自然性条件,也可以说它把态射映射到交换方块:$C$ 中每个态射,都对应 $D$ 中一个交换的自然性方块。

每个源范畴态射都给出一个交换方块

自然变换的这个性质在许多范畴构造中非常有用,而这些构造常常包含交换图。通过明智地选择函子,很多交换性条件都可以转换为自然性条件。讨论极限、余极限和伴随时,我们会看到例子。

最后,自然变换可以用于定义函子之间的同构。说两个函子自然同构,几乎就像说它们相同。自然同构定义为一个自然变换,其所有分量都是同构(可逆态射)。

10.1 多态函数(Polymorphic Functions)

我们讨论过函子,或者更具体地说,自函子在编程中的角色。它们对应把类型映射到类型的类型构造器。它们也把函数映射到函数,这个映射由高阶函数 fmap 实现(或者在 C++ 中由 transformthen 等类似函数实现)。

要构造自然变换,我们从一个对象开始,这里也就是类型 a。一个函子 F 把它映射到类型 F a。另一个函子 G 把它映射到 G a。自然变换 alphaa 处的分量是一个从 F aG a 的函数。在伪 Haskell 中:

alpha_a :: F a -> G a

自然变换是一个对所有类型 a 都有定义的多态函数:

alpha :: forall a . F a -> G a
def alpha[A]: F[A] => G[A]

在 Haskell 中,forall a 是可选的(事实上还需要打开语言扩展 ExplicitForAll)。通常会这样写:

alpha :: F a -> G a
val alpha: F[A] => G[A]

记住,它实际上是一族由 a 参数化的函数。这是 Haskell 语法简洁性的另一个例子。C++ 中的类似构造会稍微啰嗦一些:

template<class A> G<A> alpha(F<A>);

Haskell 的多态函数与 C++ 的泛型函数之间还有更深刻的差异,这体现在这些函数的实现方式和类型检查方式上。在 Haskell 中,多态函数必须对所有类型统一定义。一个公式必须适用于所有类型。这称为参数多态(parametric polymorphism)。

另一方面,C++ 默认支持 ad hoc 多态,这意味着模板不必对所有类型都有良好定义。一个模板是否适用于某个给定类型,要等到实例化时才决定;那时具体类型会替换类型参数。类型检查被推迟,不幸的是,这常常带来难以理解的错误消息。

在 C++ 中,还有函数重载和模板特化机制,允许为不同类型提供同一函数的不同定义。在 Haskell 中,这项功能由类型类和类型族提供。

Haskell 的参数多态有一个意外后果:任何如下类型的多态函数:

alpha :: F a -> G a
val alpha: F[A] => G[A]

只要 FG 是函子,就会自动满足自然性条件。用范畴记法写出来如下(f 是一个函数 f :: a -> b):

G f . alpha_a = alpha_b . F f

在 Haskell 中,函子 G 在态射 f 上的作用由 fmap 实现。先用带显式类型标注的伪 Haskell 写出来:

fmapG f . alpha_a = alpha_b . fmapF f

由于类型推断,这些标注并不必要,下面的等式成立:

fmap f . alpha = alpha . fmap f

这仍然不是真正的 Haskell 代码,因为函数相等无法在代码中表达,但它是程序员可用于等式推理的恒等式,也可由编译器用来实现优化。

自然性条件在 Haskell 中自动成立的原因,与“免费定理”(theorems for free)有关。用于在 Haskell 中定义自然变换的参数多态,对实现施加了非常强的限制:一个公式适用于所有类型。这些限制会转化为关于这类函数的等式定理。对转换函子的函数来说,免费定理就是自然性条件。

前面提过一种理解 Haskell 函子的方式:把它们看作广义容器。可以继续这个类比,把自然变换看成把一个容器中的内容重新打包进另一个容器的配方。我们不会触碰条目本身:不修改它们,也不创建新条目。只是把它们中的一些,有时是多次,复制到新的容器中。

自然性条件表达的是:先通过 fmap 修改条目再重新打包,和先重新打包再用新容器自己的 fmap 修改条目,二者没有区别。这两个动作,即重新打包和 fmap,彼此正交。“一个搬鸡蛋,另一个煮鸡蛋。”

来看几个 Haskell 中自然变换的例子。第一个在列表函子与 Maybe 函子之间。它返回列表头部,但仅在列表非空时返回:

safeHead :: [a] -> Maybe a
safeHead [] = Nothing
safeHead (x:xs) = Just x
def safeHead[A]: List[A] => Option[A] = {
case Nil => None
case x :: xs => Some(x)
}

这是一个在 a 上多态的函数。它适用于任意类型 a,没有任何限制,因此是参数多态的例子。所以它是这两个函子之间的自然变换。不过为了说服自己,验证一下自然性条件:

fmap f . safeHead = safeHead . fmap f
(fmap(f) compose safeHead) == (safeHead compose fmap(f))

需要考虑两种情形。空列表:

fmap f (safeHead []) = fmap f Nothing = Nothing
safeHead (fmap f []) = safeHead [] = Nothing
fmap(f)(safeHead(List.empty)) == fmap(f)(None) == None
safeHead(fmap(f)(List.empty)) == safeHead(List.empty) == None

非空列表:

fmap f (safeHead (x:xs)) = fmap f (Just x) = Just (f x)
safeHead (fmap f (x:xs)) = safeHead (f x : fmap f xs) = Just (f x)
fmap(f)(safeHead(x :: xs)) == fmap(f)(Some(x)) == Some(f(x))
safeHead(fmap(f)(x :: xs)) == safeHead(f(x) :: fmap(f)(xs)) == Some(f(x))

这里使用了列表的 fmap 实现:

fmap f [] = []
fmap f (x:xs) = f x : fmap f xs
def fmap[A, B](f: A => B): List[A] => List[B] = {
case Nil => Nil
case x :: xs => f(x) :: fmap(f)(xs)
}

以及 Maybe 的实现:

fmap f Nothing = Nothing
fmap f (Just x) = Just (f x)
def fmap[A, B](f: A => B): Option[A] => Option[B] = {
case None => None
case Some(x) => Some(f(x))
}

一个有趣的情形是,其中一个函子是平凡的 Const 函子。来自或指向 Const 函子的自然变换,看起来就像返回类型多态或参数类型多态的函数。

例如,length 可以看作从列表函子到 Const Int 函子的自然变换:

length :: [a] -> Const Int a
length [] = Const 0
length (x:xs) = Const (1 + unConst (length xs))
def length[A]: List[A] => Const[Int, A] = {
case Nil => Const(0)
case x :: xs => Const(1 + unConst(length(xs)))
}

这里,unConst 用于剥去 Const 构造器:

unConst :: Const c a -> c
unConst (Const x) = x
def unConst[C, A]: Const[C, A] => C = {
case Const(x) => x
}

当然,实践中 length 定义为:

length :: [a] -> Int
def length[A]: List[A] => Int

这实际上隐藏了它是自然变换这一事实。

寻找从 Const 函子出发的参数多态函数稍微困难一些,因为这要求从无中生有地创建一个值。我们能做的最好结果是:

scam :: Const Int a -> Maybe a
scam (Const x) = Nothing
def scam[A]: Const[Int, A] => Option[A] = {
case Const(x) => None
}

另一个常见函子是前面已经见过的 Reader 函子,它将在 Yoneda 引理中扮演重要角色。我会把它的定义重写为 newtype

newtype Reader e a = Reader (e -> a)
case class Reader[E, A](run: E => A)

它由两个类型参数化,但只在第二个参数上(协变地)具有函子性:

instance Functor (Reader e) where
fmap f (Reader g) = Reader (\x -> f (g x))
implicit def readerFunctor[E] = new Functor[Reader[E, ?]] {
def fmap[A, B](f: A => B)(g: Reader[E, A]): Reader[E, B] =
Reader(x => f(g.run(x)))
}

对每个类型 e,都可以定义从 Reader e 到任意其他函子 f 的一族自然变换。稍后会看到,这个族中的成员总是与 f e 的元素一一对应(这就是 Yoneda 引理)。

例如,考虑有一个元素 () 的 unit 类型 ()。函子 Reader () 把任意类型 a 映射到函数类型 () -> a。这些正是所有从集合 a 中选取一个元素的函数。这样的函数数量与 a 中元素数量一样多。现在考虑从这个函子到 Maybe 函子的自然变换:

alpha :: Reader () a -> Maybe a
def alpha[A]: Reader[Unit, A] => Option[A]

只有两个这样的变换,一个迟钝,一个显然:

dumb (Reader _) = Nothing
def dumb[A]: Reader[Unit, A] => Option[A] = {
case Reader(_) => None
}

以及:

obvious (Reader g) = Just (g ())
def obvious[A]: Reader[Unit, A] => Option[A] = {
case Reader(g) => Some(g())
}

(对 g 唯一能做的事,就是把它应用到 unit 值 () 上。)

事实上,正如 Yoneda 引理所预言的,它们对应 Maybe () 类型的两个元素,也就是 NothingJust ()。稍后会回到 Yoneda 引理;这里只是先稍微吊一下胃口。

10.2 超越自然性(Beyond Naturality)

两个函子之间的参数多态函数(包括 Const 函子这个边界情形)总是自然变换。由于所有标准代数数据类型都是函子,任意这些类型之间的多态函数都是自然变换。

我们还可以使用函数类型,而函数类型在其返回类型上是函子性的。可以用它们构造函子(例如 Reader 函子),并定义作为高阶函数的自然变换。

然而,函数类型在参数类型上不是协变的,而是逆变的。当然,逆变函子等价于来自反范畴的协变函子。两个逆变函子之间的多态函数,在范畴意义下仍然是自然变换,只不过它们作用在从反范畴到 Haskell 类型的函子上。

你可能还记得前面看过的逆变函子例子:

newtype Op r a = Op (a -> r)
case class Op[R, A](f: A => R)

这个函子在 a 上逆变:

instance Contravariant (Op r) where
contramap f (Op g) = Op (g . f)
implicit def opContravariant[R] = new Contravariant[Op[R, ?]] {
def contramap[A, B](f: B => A): Op[R, A] => Op[R, B] = {
case Op(g) => Op(g compose f)
}
}

可以写一个从 Op BoolOp String 的多态函数:

predToStr (Op f) = Op (\x -> if f x then "T" else "F")
def predToStr[A]: Op[Boolean, A] => Op[String, A] = {
case Op(f) => Op(x => if (f(x)) "T" else "F")
}

但由于这两个函子不是协变的,所以这不是 Hask 中的自然变换。不过,因为它们都是逆变的,所以满足“相反”的自然性条件:

contramap f . predToStr = predToStr . contramap f
(op.contramap(func) compose predToStr) == (predToStr compose op.contramap(func))

注意,由于 contramap 的签名,函数 f 的方向必须与 fmap 中使用的方向相反:

contramap :: (b -> a) -> (Op Bool a -> Op Bool b)
def contramap[A, B](f: B => A): Op[Boolean, A] => Op[Boolean, B] = {
case Op(g) => Op(g compose f)
}

是否有既不是协变函子也不是逆变函子的类型构造器?下面就是一个例子:

a -> a
A => A

这不是函子,因为同一个类型 a 同时出现在负位置(逆变位置)和正位置(协变位置)。无法为这个类型实现 fmapcontramap。因此,具有如下签名的函数:

(a -> a) -> f a
(A => A) => F[A]

其中 f 是任意函子,它不可能是自然变换。有趣的是,存在一种自然变换的推广,称为双自然变换(dinatural transformations),用于处理这类情况。讨论端时会遇到它们。

10.3 函子范畴(Functor Category)

既然已经有了函子之间的映射,也就是自然变换,那么自然会问:函子是否形成范畴?事实上,它们确实形成范畴!对每一对范畴 $C$ 和 $D$,都有一个函子范畴。这个范畴中的对象是从 $C$ 到 $D$ 的函子,态射是这些函子之间的自然变换。

必须定义两个自然变换的组合,但这很容易。自然变换的分量是态射,而我们知道如何组合态射。

确实,取一个从函子 $F$ 到 $G$ 的自然变换 alpha。它在对象 a 处的分量是某个态射:

alpha_a :: F a -> G a

我们想把 alphabeta 组合,后者是从函子 $G$ 到 $H$ 的自然变换。betaa 处的分量是态射:

beta_a :: G a -> H a

这些态射可以组合,其组合又是一个态射:

beta_a . alpha_a :: F a -> H a

我们把这个态射用作自然变换 beta . alpha 的分量,也就是两个自然变换 betaalpha 之后的组合:

(beta . alpha)_a = beta_a . alpha_a

自然变换的垂直组合

只要长久地看一眼图,就能说服自己:这个组合的结果确实是从 $F$ 到 $H$ 的自然变换:

H f . (beta . alpha)_a = (beta . alpha)_b . F f

组合后的自然性方块

自然变换的组合满足结合律,因为它们的分量是普通态射,而普通态射的组合满足结合律。

最后,对每个函子 $F$,都有一个恒等自然变换 1_F,它的分量是恒等态射:

id_(F a) :: F a -> F a

所以,函子确实形成范畴。

关于记法说一句。跟随 Saunders Mac Lane,我使用点号表示刚才描述的自然变换组合。问题在于,自然变换有两种组合方式。刚才这种称为垂直组合,因为在描述它的图中,函子通常是竖直堆叠起来的。垂直组合在定义函子范畴时很重要。我稍后会解释水平组合。

范畴 $C$ 与 $D$ 之间的函子范畴写作 Fun(C, D),或 [C, D],有时也写作 D^C。最后这个记法暗示,函子范畴本身或许可以被看作某个其他范畴中的函数对象(指数对象)。这确实如此吗?

函子范畴

看看目前为止已经建立起来的抽象层级。我们从范畴开始,它是对象和态射的集合。范畴本身,严格来说是小范畴(对象形成集合的范畴),也是更高层范畴 $Cat$ 中的对象。这个范畴中的态射是函子。$Cat$ 中的 hom-set 是函子的集合。例如,Cat(C, D) 是两个范畴 $C$ 和 $D$ 之间的函子集合。

函子范畴 [C, D] 也是两个范畴之间的函子集合(再加上作为态射的自然变换)。它的对象与 Cat(C, D) 的成员相同。而且,函子范畴作为一个范畴,本身必须是 $Cat$ 中的对象(碰巧两个小范畴之间的函子范畴本身也是小的)。我们在一个范畴中的 hom-set 和同一个范畴中的一个对象之间建立了关系。这种情况正像上一节看到的指数对象。看看如何在 $Cat$ 中构造后者。

函子范畴作为 Cat 中的指数对象

你可能还记得,为了构造指数对象,必须先定义积。在 $Cat$ 中,这相对容易,因为小范畴是对象集合,而我们知道如何定义集合的笛卡尔积。因此,乘积范畴 $C x D$ 中的对象只是一对对象 (c, d),其中一个来自 $C$,另一个来自 $D$。类似地,两对对象 (c, d)(c', d') 之间的态射,是一对态射 (f, g),其中 f :: c -> c'g :: d -> d'。这些态射对按分量组合,并且总有一个恒等对,也就是一对恒等态射。长话短说,$Cat$ 是一个完整的笛卡尔闭范畴,其中任意一对范畴都有一个指数对象 D^C。而我说的 $Cat$ 中的“对象”是一个范畴,所以 D^C 是一个范畴,可以把它等同于 $C$ 与 $D$ 之间的函子范畴。

10.4 2-范畴(2-Categories)

把这一点放在一边,仔细看看 $Cat$。根据定义,$Cat$ 中任意 hom-set 都是一组函子。但正如我们已经看到的,两个对象之间的函子拥有比集合更丰富的结构。它们形成一个范畴,其中自然变换作为态射。既然函子在 $Cat$ 中被视为态射,那么自然变换就是态射之间的态射。

这种更丰富的结构是 2-范畴 的一个例子。2-范畴是范畴的推广,其中除了对象和态射(在这个语境中可称为 1-态射)之外,还有 2-态射,也就是态射之间的态射。

在把 $Cat$ 看作 2-范畴时,有:

  • 对象:(小)范畴;
  • 1-态射:范畴之间的函子;
  • 2-态射:函子之间的自然变换。

作为 2-范畴的 Cat

在两个范畴 $C$ 和 $D$ 之间,不再是 hom-set,而是 hom-category,也就是函子范畴 D^C。我们有普通函子组合:从 D^C 出发的函子 $F$ 与从 E^D 出发的函子 $G$ 组合,得到从 E^C 出发的 G . F。但每个 hom-category 内部也有组合,也就是函子之间自然变换(2-态射)的垂直组合。

在一个 2-范畴中有两种组合,于是问题出现了:它们如何彼此作用?

取 $Cat$ 中两个函子,也就是 1-态射:

F :: C -> D
G :: D -> E

以及它们的组合:

G . F :: C -> E

假设有两个自然变换 alphabeta,分别作用在函子 $F$ 与 $G$ 上:

alpha :: F -> F'
beta :: G -> G'

注意,不能对这一对应用垂直组合,因为 alpha 的目标不同于 beta 的源。事实上,它们属于两个不同的函子范畴:D^CE^D。不过,可以对函子 F'G' 应用组合,因为 F' 的目标就是 G' 的源,也就是范畴 $D$。函子 G' . F'G . F 之间是什么关系?

水平组合的设置

手中有 alphabeta,能否定义一个从 G . FG' . F' 的自然变换?让我勾勒这个构造。

像往常一样,从 $C$ 中的对象 a 开始。它的像分裂为 $D$ 中两个对象:F aF' a。还有一个态射,也就是 alpha 的分量,连接这两个对象:

alpha_a :: F a -> F' a

从 $D$ 到 $E$ 时,这两个对象进一步分裂为四个对象:G(F a)G'(F a)G(F' a)G'(F' a)。我们也有四个态射形成一个方块。其中两个态射是自然变换 beta 的分量:

beta_(F a) :: G(F a) -> G'(F a)
beta_(F' a) :: G(F' a) -> G'(F' a)

另外两个是 alpha_a 在两个函子下的像(函子会映射态射):

G alpha_a :: G(F a) -> G(F' a)
G' alpha_a :: G'(F a) -> G'(F' a)

水平组合中的自然性方块

态射相当多。我们的目标是找到一个从 G(F a)G'(F' a) 的态射,作为连接两个函子 G . FG' . F' 的自然变换分量候选。事实上,从 G(F a)G'(F' a) 不止一条路径,而是两条:

G' alpha_a . beta_(F a)
beta_(F' a) . G alpha_a

幸运的是,它们相等,因为我们形成的方块正是 beta 的自然性方块。

我们刚刚定义了从 G . FG' . F' 的自然变换的一个分量。只要有足够耐心,证明这个变换的自然性相当直接。

这个自然变换称为 alphabeta水平组合

beta o alpha :: G . F -> G' . F'

同样跟随 Mac Lane,我用小圆圈表示水平组合,不过你也可能看到有人用星号代替。

这里有一条范畴论经验法则:每当有组合,就应该寻找一个范畴。我们有自然变换的垂直组合,它是函子范畴的一部分。那么水平组合呢?它存在于哪个范畴中?

弄清这一点的方式,是从侧面看 $Cat$。不要把自然变换看成函子之间的箭头,而要把它们看成范畴之间的箭头。自然变换位于两个范畴之间,也就是它所变换的那些函子连接的两个范畴之间。可以把它看作连接这两个范畴。

关注 $Cat$ 的两个对象,也就是范畴 $C$ 和 $D$。存在一组自然变换,它们位于连接 $C$ 到 $D$ 的函子之间。这些自然变换就是我们从 $C$ 到 $D$ 的新箭头。同理,也有位于连接 $D$ 到 $E$ 的函子之间的自然变换,可以把它们看作从 $D$ 到 $E$ 的新箭头。水平组合就是这些箭头的组合。

我们也有一个从 $C$ 到 $C$ 的恒等箭头。它是把 $C$ 上的恒等函子映射到自身的恒等自然变换。注意,水平组合的恒等元也是垂直组合的恒等元,但反过来不成立。

最后,这两种组合满足交换律:

(beta' . alpha') o (beta . alpha) = (beta' o beta) . (alpha' o alpha)

交换律

这里引用 Saunders Mac Lane 的话:读者也许会乐于写下证明这一事实所需的显然图表。

还有一条将来可能有用的记法。在这种从侧面解释 $Cat$ 的方式中,从对象到对象有两种方式:使用函子,或者使用自然变换。不过,我们可以把函子箭头重新解释为一种特殊的自然变换:作用在这个函子上的恒等自然变换。所以你经常会看到这种记法:

F o alpha

其中 $F$ 是从 $D$ 到 $E$ 的函子,而 alpha 是两个从 $C$ 到 $D$ 的函子之间的自然变换。由于不能真正把函子和自然变换组合起来,所以它被解释为恒等自然变换 1_Falpha 之后的水平组合。

类似地:

alpha o F

alpha1_F 之后的水平组合。

10.5 结语(Conclusion)

这就结束了本书第一部分。我们已经学会了范畴论的基本词汇。可以把对象和范畴看作名词,把态射、函子和自然变换看作动词。态射连接对象,函子连接范畴,自然变换连接函子。

不过我们也看到,在一个抽象层次上表现为动作的东西,在下一个层次上会变成对象。态射的集合变成函数对象。作为对象,它可以成为另一个态射的源或目标。这就是高阶函数背后的思想。

函子把对象映射到对象,所以可以把它用作类型构造器,或者参数化类型。函子也映射态射,所以它是一个高阶函数,也就是 fmap。有一些简单函子,如 Const、积和余积,可以用来生成大量代数数据类型。函数类型同样是函子性的,既有协变也有逆变,并且可以用来扩展代数数据类型。

函子可以被看作函子范畴中的对象。因此,它们成为态射的源和目标,而这些态射就是自然变换。自然变换是一种特殊类型的多态函数。

10.6 挑战(Challenges)

  1. 定义一个从 Maybe 函子到列表函子的自然变换。证明它满足自然性条件。
  2. Reader () 与列表函子之间定义至少两个不同的自然变换。有多少个不同的 () 列表?
  3. Reader BoolMaybe 继续上一题。
  4. 说明自然变换的水平组合满足自然性条件(提示:使用分量)。这是一次很好的追图练习。
  5. 写一篇短文,讲讲你会如何享受写下证明交换律所需的那些显然图表。
  6. 为不同 Op 函子之间变换的相反自然性条件创建几个测试用例。下面是一个选择:
op :: Op Bool Int
op = Op (\x -> x > 0)

以及:

f :: String -> Int
f x = read x