原文见 http://bartoszmilewski.com/20...
-> 上一篇:『简单的代数数据类型』
听起来是要破记录,我要讲讲函子:简单又强大的主意。范畴论中充满了这样简单又强大的主意。函子是范畴之间的映射。给定的两个范畴,C 与 D,函子 F 可以将 C 中的对象映射为 D 中的对象——函子是对象上的函数。如果 C 中有一个对象 a,它在 D 中的像即为 F a(省略了括号)。但是范畴中不仅仅存在对象,还有连接对象的态射。函子也可以映射态射——函子是态射上的函数。不过,它不能随意的映射态射——它需要保持态射的结构。因此,如果 C 中有一个态射 f,它连接对象 a 与对象 b,
f :: a -> b
那么 f 在 D 中的像就是 F f,它连接了 a 在 D 中的像与 b 在 D 中的像:
F f :: F a -> F b
以上陈述中夹杂了数学与 Haskell 记号,希望这样有意义。对于作用于对象或态射的函子,我没有使用括号。
可以看到,函子保持了范畴的结构:在一个范畴中被态射连接的东西在另一个范畴中依然被类似的态射所连接。但是范畴的结构还包含更多的东西,即态射的复合。如果 h 是 f 与 g 的复合:
h = g . f
我们希望它被 F 映射的像是 f 的像与 g 的像的复合:
F h = F g . F f
最后,我们希望 C 中的恒等态射被映射为 D 中的恒等态射:
$$ F\; id_a = id_{F\; a} $$
在此,$id_a$ 是作用于对象 a 的恒等态射,而 $id_{F\;a}$ 是作用于对象 F a 的恒等态射。
注意,这些条件使得函子要比常规的函数更为严于律己。函子必须保持范畴的结构。如果将一个范畴比喻为对象与态射组成的一张网,函子作用于这张网的过程中不能有一点的撕裂。它可能会将一些对象打碎,也可能会将多个态射合并为一个,但是它永远不能将东西从网中分割出去。这种保持网不被撕裂的约束类似于代数中的连续性条件,也就是说或函子具有『连续性』(尽管函子的连续性还存在更多的限定概念)。就像函数一样,函子可以做折叠或嵌入的工作。所谓嵌入,就是将一个小的源范畴嵌入到更大的目标范畴中。一个极端的例子,源范畴是个单例范畴——只有一个对象与一个态射(恒等态射)的范畴,从单例范畴映射到任何其他范畴的函子,所做的工作就是在后者中选择一个对象。这完全类似于接受单例集合的态射,这个态射会从目标集合中选择元素。最巨大的折叠函子被称为常函子 $\triangle_C$,它将源范畴中的每个对象映射为目标范畴中特定的对象 c,它也可以将源范畴中的每个态射映射为目标范畴中的特定的恒等态射 $id_c$,它在行为上像一个黑洞,将所有东西压成一个奇点。在讨论极限与余极限时,我们再来考察这个黑洞函子。
编程中的函子
现在回到地球,谈谈编程。我们有了类型与函数构成的范畴。如果不知道该用函子将这个范畴映射为别的什么范畴,那就看看怎么用函子将这个范畴映射为其自身——这样的函子被称为自函子。类型范畴中的一个自函子是什么样子的?首先,它将类型映射为类型。我们已经碰到过这种映射的例子了,只是你没有意识到这一点。实际上这就是将其他类型作为参数的类型定义。下面来看几个例子。
Maybe 函子
Maybe
的定义就是将类型 a
映射为类型 Maybe a
:
data Maybe a = Nothing | Just a
微妙之处在于:Maybe
本身不是一个类型,它是一个类型构造子(Constructor)。必须向它提供一个类型参数,例如 Int
或 Bool
,然后才可以使其变成一个类型。如不果不向 Maybe
提供任何参数,那么它就是一个作用于类型的函数。不过,我们能将 Maybe
变成函子么?(从现在开始,当在编程环境中我所提到的函子,指的是自函子)一个函子不仅仅只映射对象(在此,是类型),它也映射态射(在此,是函数)。对于任何从 a
到 b
的函数:
f :: a -> b
要定义一个从 Maybe a
到 Maybe b
的函数,需要考虑两种情况,它们对应于 Maybe
的两个构造子。若这个函数的参数是 Nothing
,那么返回 Nothing
即可。若这个函数的参数是 Just
,就将 f
应用于 Just
的内容。因此,f
被 Maybe
函子映射为:
f' :: Maybe a -> Maybe b
f' Nothing = Nothing
f' (Just x) = Just (f x)
(顺便说一下,Haskell 允许在变量名中使用 '
符号,非常方便。)Haskell 以高阶函数的形式实现了一个函子的态射映射部分,这个函数叫 fmap
。对于 Maybe
的情况,这个函数的签名如下:
fmap :: (a -> b) -> (Maybe a -> Maybe b)
通常说 fmap
提升了一个函数。被提升的函数作用于 Maybe
层次上的值。由于柯里化(Curring)的缘故,fmap
的签名有两种解释:作为带有单个参数的函数——这个参数本身是个函数 (a -> b)
——返回一个函数 (Maybe a -> Maybe b)
;或者是带有两个参数的函数,返回 Maybe b
:
fmap :: (a -> b) -> Maybe a -> Maybe b
综上所述,对于 Maybe
而言,fmap
的定义如下:
fmap _ Nothing = Nothing
fmap f (Just x) = Just (f x)
为了说明类型构造子 Maybe
携同函数 fmap
共同形成一个函子,不得不证明 fmap
能够维持恒等态射以及态射的复合的存在。所证明的东西,叫做『函子定律』。凡是满足函子定律的函子,必定不会破坏范畴的结构。
等式推导
为了证明函子定律,我需要借助等式推导,这也是 Haskell 中常用的证明技巧。它利用了 Haskell 函数基于等式定义这一优势:左侧等于右侧。总是可以用其中一侧替换另一侧,只是有时变量名需要改一下以避免名字冲突。可以将这种替换视为内联一个函数,或者将一个表达式重构为一个函数。以恒等函数为例:
id x = x
如果你在一些表达式中看到 id y
,你可以将其替换为 y
,这就是内联。如果你看到 id
被应用到一个表达式,例如 id (y + 2)
,你可以用这个表达式本身 (y + 2)
来替换它。这种替换可以从两个方向进行:你可以用 id e
来替换 e
(重构)。如果一个函数是基于模式匹配定义的,可以单独使用它的子定义。例如,对于上述 fmap
的定义,你可以用 Nothing
替换 fmap f Nothing
,也可以反方向替换。下面来看如何运用等式推导。先从证明函子对恒等态射的维持开始:
fmap id = id
要考虑两种情况:Nothing
与 Just
。第一种情况(下面从左侧到右侧的变换,用的是 Haskell 伪代码):
fmap id Nothing
= { definition of fmap }
Nothing
= { definition of id }
id Nothing
注意,上面的最后一步,我反向使用了 id
的定义。表达式 Nothing
被我替换为 id Nothgin
。在实践中,你可以运用这种『从两头点蜡烛』式的证明手段,直到它们在中间碰到相同的表达式——在此就是 Nothgin
。第二种情况也很容易:
fmap id (Just x)
= { definition of fmap }
Just (id x)
= { definition of id }
Just x
= { definition of id }
id (Just x)
现在来证明 fmap
能够维持态射的复合:
fmap (g . f) = fmap g . fmap f
首先来看 Nothing
对应的情况:
fmap (g . f) Nothing
= { definition of fmap }
Nothing
= { definition of fmap }
fmap g Nothing
= { definition of fmap }
fmap g (fmap f Nothing)
再来看 Just
对应的情况:
fmap (g . f) (Just x)
= { definition of fmap }
Just ((g . f) x)
= { definition of composition }
Just (g (f x))
= { definition of fmap }
fmap g (Just (f x))
= { definition of fmap }
fmap g (fmap f (Just x))
= { definition of composition }
(fmap g . fmap f) (Just x)
需要强调一下,带有副作用的 C++ 『函数』是不能用等式推导的,请看:
int square(int x) {
return x * x;
}
int counter() {
static int c = 0;
return c++;
}
double y = square(counter());
使用等式推导,你可以内联 square
,得到:
double y = counter() * counter();
结果绝对不是一个有效的变换,因为它无法产生相同的结果。如果使用宏来定义 square
,C++ 编译器会尝试使用等式推导,但是结果可能挺悲催。
Optional
在 Haskell 中很容易表示函子,但是在其他任何支持泛型编程与高阶函数的语言中也能够定义函子。C++ 中的 Maybe
是模板类型 optional
,下面是它的粗略实现(真实的实现相当复杂,需要处理参数传递的多种方式,要用到 Copy 构造函数以及资源管理等 C++ 特色的东西):
template
class optional {
bool _isValid; // the tag
T _v;
public:
optional() : _isValid(false) {} // Nothing
optional(T x) : _isValid(true) , _v(x) {} // Just
bool isValid() const { return _isValid; }
T val() const { return _v; }
};
这个模板提供了函子定义的一部分:类型的映射。它将任意类型 T
映射为一个新的类型 optional
。函子定义的另一部分,即函数的映射,实现如下:
template
std::function(optional)>
fmap(std::function f)
{
return [f](optional opt) {
if (!opt.isValid())
return optional{};
else
return optional{ f(opt.val()) };
};
}
这是个高阶函数,它接受一个函数作为参数,然后返回一个函数。它的非柯里化版本如下:
template
optional fmap(std::function f, optional opt) {
if (!opt.isValid())
return optional{};
else
return optional{ f(opt.val()) };
}
也可以将 fmap
定义为 optional
的模板方法。这种选择上的困窘,使得函子模式在 C++ 中的抽象变成一个问题。是让函子作为接口来继承(不幸的是,模板类型是不能拥有虚函数的),还是让函子作为柯里化或非柯里化的自由的模板函数?是让 C++ 编译器去正确的推导类型,还是显式的指定类型?思考一下,若函子接受一个从 int
到 bool
的函数 f
,那么编译器当如何确定 g
的类型:
auto g = fmap(f);
特别是,在未来,如果有多个重载了 fmap
的函子呢?(很快我们就会看到更多的函子)
类型类
Haskell 如何对函子进行抽象?它使用类型类。一个类型类定义了支持一个公共接口的类型族。例如,支持相等谓词的类型类如下:
class Eq a where
(==) :: a -> a -> Bool
这个定义陈述的是,如果类型 a
支持 (==)
运算符,那么它就是 Eq
类。(==)
运算符接受两个类型为 a
的值,返回 Bool
值。如果你想告诉 Haskell 有一个特定的类型是 Eq
类,那么你不得不将其声明为这个类的一个实例,并提供 (==)
的实现。例如,一个二维 Point
(两个 Float
的积类型):
data Point = Pt Float Float
需要为它定义相等谓词:
instance Eq Point where
(Pt x y) == (Pt x' y') = x == x' && y == y'
这里我将 (==)
作为中缀运算符使用,它处于 (Pt x y)
与 (Pt x' y')
之间,而函数体是单个 =
号后面的部分。一旦将 Point
声明为 Eq
的一个实例,你就可以直接比较点与点是否相等了。注意,与 C++ 或 Java 不同,在定义 Point
的时候不必指定它是 Eq
类(或接口)的实例——可在真正需要的时候再指定。类型类是 Haskell 仅有的函数(运算符)重载机制。在为不同的函子重载 fmap
时需要借助类型类,尽管有一个难点:函子不能作为类型来定义,只能作为类型的映射来定义,即类型构造子。我们需要一个由类型构造子构成的族,而不是像 Eq
这样的类型族。所幸,Haskell 的类型类可以将类型构造子像类型那样来处理。因此,Functor
类的定义如下:
class Functor f where
fmap :: (a -> b) -> f a -> f b
如果存在符合上述签名的 fmap
函数,这个类规定了 f
是一个 Functor
。小写的 f
是一个类型变量,类似于类型变量 a
与 b
,然而编译器能够推断出它是一个类型构造子,而不是一个类型,依据是它的用途:作用于其他类型,即 f a
与 f b
。因此,要声明一个 Functor
的实例时,你不得不给它一个类型构造子,对于 Maybe
而言就是:
instance Functor Maybe where
fmap _ Nothing = Nothing
fmap f (Just x) = Just (f x)
顺便说一下,Functor
类,以及它的一些实例,这些实例是为大多数简单的数据类型而定义的,包括 Maybe
,它们都是 Haskell 标准库的一部分。
C++ 中的函子
在 C++ 中可以弄成类似的函子类么?一个类型构造子对应于一个模板类,例如 optional
,因此作同样的类比,我们可以用一个*模板的模板的参数 F
来参数化 fmap
,即:
template F, class A, class B>
F fmap(std::function, F);
对于不同的函子,我们想对这个模板进行特化。不幸的是,在 C++ 中是禁止模板函数的部分特化的。所以,你不能这样写:
template
optional fmap(std::function f, optional opt)
我们只能再回到函数重载的老路上,回到那个非柯里化版本的 fmap
的原始定义:
template
optional fmap(std::function f, optional opt)
{
if (!opt.isValid())
return optional{};
else
return optional{ f(opt.val()) };
}
这个定义能够工作,但只是因为编译器根据 fmap
第二个参数选择了一个重载版本的 fmap
,它完全忽略了那个更为泛型的 fmap
定义。
List 函子
为了对编程中的函子所扮演的角色获得一些直觉,我们需要看更多的例子。任意一个被其他类型参数化了的类型,都是一个候选的函子。通用的容器被它们所存储的类型参数化了,来看一个最简单的容器——列表:
data List a = Nil | Cons a (List a)
List
是类型构造子,它将任意类型 a
映射为类型 List a
。为了表明 List
是一个函子,我们不得不定义一个提升函数:接受一个 a -> b
的函数,产生一个 List a -> List b
的函数:
fmap :: (a -> b) -> (List a -> List b)
作用于 List a
的函数必须要考虑两种情况,因为存在两个列表构造子。Nil
对应情况很简单——只需返回 Nil
——你不可能对一个空的列表做别的什么事。Cons
对应的情况有点麻烦,因为它包含着递归。我们先退一步,考虑一下正在干什么。我们有一个 a
的列表(包含类型 a
的值的列表),还有一个从 a
到 b
的函数,我们想得到的是一个 b
的列表(包含类型 b
的值的列表)。显然,我们要用 f
将 a
的列表中的每一个元素映射到 b
的列表中。假设各定的是由 Cons
定义了首尾的一个(非空的)列表,如何实现这样的函数?我们可以将 f
作用于列表的首部,然后将提升后的(fmap
过的)f
作用于列表的尾部。这依然是一个递归的定义,因为我们是以提升的 f
来定义提升的 f
:
fmap f (Cons x t) = Cons (f x) (fmap f t)
注意,等式的右侧,fmap f
作用于一个列表,而这个列表的长度小于等式左侧传入的那个列表——前者是后者的尾部。这个递归过程逐渐将列表缩短,最终会抵达一个空的列表,即 Nil
。由于我们已经定义了作用于 Nil
的 fmap f
的返回结果是 Nil
,因此递归过程就终止了。为了得到最终的结果,我们将新的首部 (f x)
与新的尾部 (fmap f t)
使用 Cons
构造子组装起来。将上述所定义的东西放到一起,就得到了列表函子的实例声明:
instance Functor List where
fmap _ Nil = Nil
fmap f (Cons x t) = Cons (f x) (fmap f t)
如果你足够熟悉 C++,会想到 std::vector
,它是 C++ 中应用最为广泛的容器。面向 std::vector
的 fmap
可以通过薄层封装 std::transform
来实现:
template
std::vector fmap(std::function f, std::vector v)
{
std::vector w;
std::transform( std::begin(v)
, std::end(v)
, std::back_inserter(w)
, f);
return w;
}
我们可以用这个函数来计算一系列数值的平方:
std::vector v{ 1, 2, 3, 4 };
auto w = fmap([](int i) { return i*i; }, v);
std::copy( std::begin(w)
, std::end(w)
, std::ostream_iterator(std::cout, ", "));
大部分 C++ 容器都是函子,它们依赖于可以传给 std::transform
的迭代器。不行的是,函子的单纯性在迭代器与临时建筑(看看上面的 fmap
的实现)的混乱背景下丧失了。我很高兴的宣布,新的 C++ range 库更加函数化了。
Reader 函子
你已经对函子获得一些直觉了——例如,函子是某种容器——下面来个有点烧脑的例子。考虑一个从类型 a
到一个返回 a
的函数类型的映射。我们不去深入的探讨函数类型——这需要全面的范畴知识——不过,我们是程序猿,对函数类型总是有一些认识的。在 Haskell 中,函数类型是使用箭头类型构造子 (->)
构造出来的,这个类型构造子接受两种类型:参数类型与返回类型。你已经见过这个类型构造子的中缀形式 a -> b
,但是也可以写成前缀形式,像是被参数化了:
(->) a b
就像常规的函数一样,接受多个参数的类型函数可以偏应用。因此,当我们向箭头只提供一个参数时,它依然期待另一个参数的出现。因此
(->) a
也是个类型构造子。它需要一个类型 b
来产生完整的类型 a -> b
。它所表示的是,它定义了一族由 a
参数化的类型构造子。我们看一下是不是还有个函子族。处理两个类型参数可能有点混乱,先做一些重命名的工作。在我们之前的函子定义中,我们可以将参数类型称为 r
,将返回类型称为 a
。因此我们的类型构造子可以接受任意类型 a
,并将其映射为类型 r -> a
。为了表明它是个函子,我们就需要一个函数,它可以将函数 a ->b
提升为一个从 r -> a
到 r -> b
的函数,而 r -> a
与 r -> b
就是 (->) r
这个类型构造子分别作用于 a
与 b
所产生的函数类型。以上讨论最终可归结为 fmap
的函数签名:
fmap :: (a -> b) -> (r -> a) -> (r -> b)
我们不得不解决一个难题:对于给定的函数 f :: a -> b
与 g :: r -> a
,构造一个函数 r -> b
。这是复合两个函数的唯一途径,也恰恰就是我们需要的。因此,我们需要将 fmap
的实现为:
instance Functor ((->) r) where
fmap f g = f . g
这就是我们想要的 fmap
!如果你喜欢紧凑一些的表示,可以先将上面等式的右侧改为前缀表示:
fmap f g = (.) f g
然后忽略等公两侧的参数:
fmap = (.)
类型构造子 (->) r
与上面这个 fmap
组合起来所形成的函子被称为 Reader 函子。
作为容器的函子
我们已经见识了编程语言中定义了通用容器的函子,至少它们定义了一些包含了某些类型的值的对象,也就是说这些对象被自身所包含的值的类型参数化了。Reader 函子看上去是个异类,因为我们没有想过将函数视为数据。不过,我们已经见过纯函数是可以被保存下来的,函数的执行结果被扔到可检索的表中,而表是数据。反之,由于 Haskell 具有惰性计算能力,一个传统的容器,譬如一个列表,实际上可以被实现为一个函数。例如,一个包含自然数的无限长的列表,可被定义为:
nats :: [Integer]
nats = [1..]
第一行,方括号是 Haskell 内建的列表类型构造子。第二行,方括号用于构造列表数据。显然,一个无限长的列表是不能存储在内存中的。编译器将其实现为一个可以按需产生一组 Integer
值的函数。Haskell 有效的模糊了数据与代码的区别。可以将列表视为函数,也可以将函数视为从存储着参数与结果的表中查询数据。如果函数的定义域有界并且不太大,将函数变成表查询是完全可行的。strlen
不能变成表查询,因为有无限多的不同的字符串。作为程序猿,我们不喜欢无限,但是在范畴论中无限是家常便饭。无论是所有字符串的集合还是宇宙所有可能状态的集合——过去,现在与未来——总是可以在范畴论中处理!因此,我喜欢将函子对象(由自函子产生的类型的实例)视为包含着一个值或多个值的容器,即使这些值实际上并未出场。C++ 中的 std::future
函子在某些时间点上可以存储一个值,但是它不能担保这个值总是存在;如果你想访问这个值,可能会受阻,直到其它线程的计算过程。另一个例子是 Haskell 的 IO
对象,它包含着用户的输入,或者是我们的宇宙要显示于屏幕上的『Hello World!』的未来版本。根据这种解释,函子对象就是可以包含一个值或多个值的容器,这些值的类型参数化了函子对象。或者,函子对象可能包含产生这些值的方法。我们根本不关心能否访问这些值——这些事发生在函子作用范围之外。如果函子对象包含的值能够被访问,我们就可以看到相应的操作结果;如果它们不能被访问,我们所关心的只是操作的正确复合,以及伴随不改变任何事物的恒等函数的操作。为了向你表明我们是如何的不关心函子对象内部的值,这里有一个类型构造子,它完全的忽略参数 a
:
data Const c a = Const c
Const
类型构造子接受两种类型, c
与 a
。就像我们处理箭头构造子那样,我们对其进行偏应用从而制造了一个函子。数据构造子(也叫 Const
)仅接受 c
类型的值,它不依赖 a
。与这种类型构造子相配的 fmap
类型为:
fmap :: (a -> b) -> Const c a -> Const c b
因为这个函子是忽略类型参数的,所以 fmap
的实现也可以自由忽略那个函数参数——因为这个函数无事可做:
instance Functor (Const c) where
fmap _ (Const v) = Const v
在 C++ 中可能更清晰一些(我从未想过我居然会这样说!),因为 C++ 中在类型参数与值之间有着明显的区别,前者出现于编译期间,后者出现于运行时:
template
struct Const {
Const(C v) : _v(v) {}
C _v;
};
C++ 版本的 fmap
也可以忽略那个函数参数,本质上就是不改变 Const
参数所包含的值的类型转换:
template
Const fmap(std::function f, Const c) {
return Const{c._v};
}
尽管它有些怪异,但是 Const
函子在许多结构中扮演着重要的角色。在范畴论中,它是 $\triangle_C$ 函子的特例,后者我们在前面提到过的,就是那个黑洞函子,而 Const
是个黑洞自函子。将来,我们还会碰到它。
函子的复合
让自己相信范畴之间的函子可以复合并不太难,函子的复合类似于集合之间的函数复合。两个函子的复合,就是两个函子分别对各自的对象进行映射的复合,对于态射也是这样。恒等态射穿过两个函子之后,它还是恒等态射。复合的态射穿过两个函子之后还是复合的态射。函子的复合只涉及这些东西。特别是,自函子很容易复合。还记得 maybeTail
么?下面我用 Haskell 内建的列表来重新实现它(用 []
替换 Nil
,用 :
替换 Cons
):
maybeTail :: [a] -> Maybe [a]
maybeTail [] = Nothing
maybeTail (x:xs) = Just xs
maybeTail
返回的结果是两个作用于 a
的函子 Maybe
与 []
复合后的类型。这些函子,每一个都配备了一个 fmap
,但是如果我们想将一个函数 f
作用于复合的函子 Maybe []
所包含的内容,该怎么做?我们不得不突破两层函子的封装:使用 fmap
突破 Maybe
,再使用 fmap
突破列表。例如,要对一个 Maybe [Int]
中所包含的元素求平方,可以这样做:
square x = x * x
mis :: Maybe [Int]
mis = Just [1, 2, 3]
mis2 = fmap (fmap square) mis
经过类型分析,对于外部的 fmap
,编译器会使用 Maybe
版本的;对于内部的 fmap
,编译器会使用列表版本的。于是,上述代码也可以写为:
mis2 = (fmap . fmap) square mis
不过,要记住,fmap
可以看作是只接受一个参数的函数:
fmap :: (a -> b) -> (f a -> f b)
在我们的示例中,(fmap . fmap)
中的第 2 个 fmap
所接受的参数是:
square :: Int -> Int
然后返回一个这种类型的函数:
[Int] -> [Int]
第一个 fmap
接受这个函数,然后再返回一个函数:
Maybe [Int] -> Maybe [Int]
最终,这个函数作用于 mis
。因此两个函子的复合结果,依然是函子,并且这个函子的 fmap
是那两个函子对应的 fmap
的复合。现在回到范畴论:显然函子的复合是遵守结合律的,因为对象的映射遵守结合律,态射的映射也遵守结合律。在每个范畴中也有一个恒等函子:它将每个对象都映射为其自身,将每个态射映射为其自身。因此在某个范畴中,函子具有与态射相同的性质。什么范畴会是这个样子?必须得有一个范畴,它包含的对象是范畴,它包含的态射是函子。也就是说,它是范畴的范畴。但是,所有范畴的范畴不得不包含它自身,这样我们就陷入了自相矛盾的境地,就像不可能存在集合的集合那样。然而,有一个叫做 Cat 的范畴,它包含了所有的小范畴。这个范畴是一个大的范畴,因此它就不可能是它自身的成员。所谓的小范畴,就是它包含的对象可以形成一个集合,而不是某种比集合还大的东西。请注意,在范畴论中,即使一个无限的不可数的集合也被认为是『小』的。我想我已经提到过这样的集合了,因为我们已经看过同样的结构在许多抽象层次上的重复出现。以后我们也会看到函子也能形成范畴。
挑战
1.
下面的定义可以将类型构造子 Maybe
变成一个函子吗?(提示:检查它是否符合函子定律)
fmap _ _ = Nothing
2.
证明 Reader 函子符合函子定律。(提示:这相当简单。)
3.
用你第二喜欢的语言实现 Reader 函子(第一个当然是 Haskell)。
4.
证明列表函子符合函子定律。假设列表尾部函子符合函子定律(换句话说,使用归纳法。)
致谢
Gershom Bazerman 相当友善的审阅了这一系列文章。非常感谢他的耐心和见解。
-> 下一篇:『函子性』