说到 Haskell,这真是一门逼格极高的编程语言,一般初学者如果没有相关函数式编程的经验,入门直接接触那些稀奇古怪的概念,简直要跪下。现在回想起来,隐隐觉得初学者所拥有的命令式编程语言(imperative programming language)相关的知识和经验反而成了负担,若能抛掉以往固有的观念转以全新的视角来看待这些新奇东西,仿佛会更好接受些,真是莫名其妙。
Bartosz Milewski 在其博客上写了不少 Haskell 及函数式编程相关的文章,读来真是受益良多,这位大哥很多年前就开始探讨 c++ 模板元编程与 Haskell 编程之间所存在的不是那么直接却又确乎存在的微妙联系,许多观点让人眼前一亮乃至发人深思,比如说从范畴论的角度来理解和解释什么是单子(monad)(接下来准备写篇博客总结一下),从函数式编程的角度来看待和进行 C++ 模板编程等,观点十分有见地且让人启发。Bartosz 讲 Haskell 喜欢从数学的角度来阐述,视角和格局非同一般,当然他不是第一位这样做的,事实上 Haskell 与数学本来就有着许多不得不说却又说不清道不明的暧昧关系(住口!)。
范畴论基本概念
如果你是第一次听说范畴论(category theory),看到这高大上的名字估计心里就会一咯噔,到底数学威力巨大,光是高等数学就能让很多人噩梦连连。和搞编程的一样,数学家喜欢将问题不断加以抽象从而将本质问题抽取出来加以论证解决,范畴论就是这样一门以抽象的方法来处理数学概念的学科,主要用于研究一些数学结构之间的关系及联系。
在范畴论里,一个范畴(category)指的是这样一个好东西,它由三部分组成:
- 一系列的对象(object).
- 一系列的态射(morphism).
- 一个组合(composition)操作符,用点(.)表示,用于将态射进行组合。
一个对象可以看成是一类东西,数学上的群,环,甚至简单的有理数,无理数等都可以归为一个对象,对应到编程语言里,可以理解为一个类型,比如说整型,布尔型,类型事实上可以看成是值的集合,例如整型就是由 0,1,2...等组成的,因此范畴论里的对象简单理解就可以看成是值(value)的集合。
一个态射指的是一种映射关系,简单理解,态射的作用就是把一个对象 A 里的值 va 映射为 另一个对象 B 里的值 vb,这和代数里的映射概念是很相近的,因此也有单射,满射等区分。态射的存在反映了对象内部的结构,这是范畴论用来研究对象的主要手法:对象内部的结构特性是通过与别的对象的关系反映出来的,动静是相对的,范畴论通过研究关系来达到探知对象的内部结构的目的。
组合操作符的作用是将两个态射进行组合,例如,假设存在态射 f: A -> B, g: B -> C, 则 g.f : A -> C.
看!好像没有想象中的复杂!一个结构要想成为一个范畴, 除了必须包含上述三样东西,它还要满足以下三个限制:
-
态射要满足结合律,即 f.(g.h) = (f.g).h。
-
态射在这个结构必须是封闭的,也就是,如果存在态射 f, g,则必然存在 h = f.g。
-
对结构中的每一个对象 A, 必须存在一个单位态射 Ia: A -> A, 对于单位态射,显然,对任意其它态射 f, f.I = f。
讲完了!范畴论就这么点东西!-- 当然是不可能的,但暂时来说,知道这些就已经很足够了。
Haskell 中的范畴
在 Haskell 中存在着这样一个唯一的范畴,名字称为 Hask, 这个 Hask 满足前面关于范畴的全部约定,因此是范畴论里一个纯正的“范畴":
-
对象就是 Haskell 里的所有类型,记得类型是一个集合。
-
态射就是编程语言里的一般函数(function),如:
func :: Int -> Bool
,将对象 int 映射为 对象 bool。 -
态射的组合就是函数的组合,在 Haskell 里,函数也是通过点号(.)进行组合的。
另外三个约束条件很容易证明也是满足,因此整个 Haskell 从数学的角度上看它就是一个范畴,这个角度的理解是很深刻的,这样一来传统意义上诸如语法,类型,函数等语言特性其实都只是这个内在本质的外在表现而已。
函子
前面对范畴的介绍反映了范畴内部各个对象之间的联系与相互作用,在范畴论里另外研究的重点是范畴与范畴之间的关系,就正如对象与对象之间有态射一样,范畴与范畴之间也存在某些映射,从而可以将一个范畴映射为另一个范畴,这种映射在范畴论中叫作函子(functor),具体来说,对于给定的两个范畴 A 和 B, 函子的作用有两个:
-
将范畴 A 中的对象映射到范畴 B 中的对象。
-
将范畴 A 中的态射映射到范畴 B 中的态射。
显然,函子反映了不同的范畴之间的内在联系,函子的定义是十分松散的,而不同范畴之间的关系有强有弱,一个随便定义的函子很多时候并不能太深刻反映范畴之间结构上的联系,因此数学上,对函子通常有几个限制,先假设 F 是范畴 A 与范畴 B 上一个函子,则:
-
对范畴 A 上的单位态射Ia, F 必须将其映射为范畴 B 上的单位态射 Ib, F(Ia) = Ib.
-
函子对态射的组合必须满足分配徤,即,假设 f, g 是范畴 A 上的态射,则 F(f.h) = F(f).F(g)。
显然这两个限制是很强的,如果两个范畴之间存在这样一个函子,则反映了他们之间在结构上有着很强的相似性,从看似风牛马不相及的东西里找出他们内在的相似性,数学家最爱干的事情了。
和态射一样函子也可以是自映射的,即函子允许将范畴映射到其自身,这样做有什么好处呢?不同范畴之间的映射反映了范畴间的相似性,范畴到范畴自身的映射则显然是反映了范畴内部的自相似性 --- 到底认识自己也不是一件容易的事啊。。。自相似性是大自然里美妙的存在,想想六角形的雪花,想想分形... 在范畴论里,这种将范畴映射到自身的函子被称为自函子(endofunctor).
Haskell 中的函子
知道为什么要讲自函子了吗,Haskell 中只有一个范畴! 那么这个唯一的范畴 Hask 中,存不存在自函子呢?有的!终于讲到重点了,为什么 Haskell 有这么些奇怪的概念? Haskell 的老鸟会告诉你,这些奇怪的东西都是宝贝,它们都是有本而来的。
那么 Haskell 中的自函子是怎么体现出来的呢? 根据前面的定义,一个函子其实就是一个映射,它把对象映射为对象,把态射映射为态射,我们知道在 Haskell 中对象就是一个类型,如整型,布尔型等,将一个类型映射为另一个类型,没错,就是 type constructor 在干的事情,c++ 的程序员可以用模板类来想象一下,如,vector<int>
其实就是将 int
映射为 vector<int>
, 这是两种不同的类型了,实例化模板的过程实际上就是把一个类型变成另一个类型的过程。
注意不要把对象的映射与对象内部的态射混淆了,态射是将对象内部的值进行映射,而对象的映射(函子)是把对象这个整体映射为另一个对象,函子根本不关心一个对象内部会有什么值。
类型到类型的映射事实上并不是普遍存在的,自函子反映的是范畴内部的结构关系,这些关系并不是因为函子的存在而存在,函子只是揭示了这些内在的关系。具体在 Haskell 中,类型间的关系并不是普遍存在的,比如说, Int -> Bool 就没有直接对应的映射关系,而存在映射关系的类型,它们都有一些共同的特点,比如可以看成是简单类型与复杂类型之间的相互转换。
type constructor 就是自函子的一部分!
好了,现在类型到类型的映射在 Haskell 中找到了,那态射到态射之间的映射呢?必竟这也是函子的必要组成部分。
在 Haskell 中,态射就是一般的函数,把一个函数映射为另一个函数,听起来不就是高阶函数在干的事情嘛。具体来说,映射函数这件事可以认为来自 Functor 这个 typeclass,连名字都一模一样,目的昭然若揭。Haskell 中的 Functor 是一个 typeclass,它的定义如下:
class Functor f where
fmap:: (a -> b) -> f a -> f b
fmap 干嘛的?显然就是用来把态射 (a -> b)
映射为态射 (f a -> f b)
的,它把范畴里的态射映射到另一个态射,且遵守了函子在映射态射时所需要遵守的两个原则。
讲到这里,我们一步一步不知不觉就已经向着 monad 靠近了,好激动,先打住了,回头再整理整理。
【参考】
http://en.wikibooks.org/wiki/Haskell/Category_theory
http://bartoszmilewski.com/2011/01/09/monads-for-the-curious-programmer-part-1/