scala与函数式编程——从范畴论看函数式编程

什么是范畴?

  生活中我们经常说:我们讲的不是一个范畴里的东西!意思就是说两个人所讲的事物不具有任何关联,没有相关性。其实范畴Category就是指一群事物以及这些事物之间的所有关联关系,这些事物和这些关联关系共同组成了某个范畴。
  举一个例子,比如在动物这个范畴中,,螳螂,黄雀都是动物这个范畴内的一个事物(即元素),然而螳螂的天敌,黄雀又是螳螂的天敌,这些就是这些事物之间的关系。如果将每种具体的动物画作一个点,那么就可以用带箭头的线将很多点连接起来,每个点上都可以有数不表的箭头,从而形成一幅复杂无缘的图。形成的这张图(箭头和点)就是我们所说的动物这个范畴。
  因此,当我们在说范畴的时候,我们谈论的其实是相关的所有元素以及这些元素之间的关系,而范畴的名称其实只是一个代号而已。
  再举一个例子,在百度员工这个范畴中,所有的百度员工都是这个范畴下的一个点,然后A员工是B的上级,B是C的同级,诸如此类的人与人之间的职位、层级关系就是这个范畴中的箭头。举这个例子的目的是为了体现动物这个范畴的特殊之处:与百度员工这个范畴做对比就会发现,百度员工范畴内的每个元素是一个普通的点,而动物这个范畴内的每个元素其实是一个集合。螳螂这个点代表的是螳螂这个物种,而黄雀也并不是真的指某只具体的黄雀。

编程所描绘的范畴

  那么,当我们的代码与范畴又有什么关系呢?在谈论编程时,我们所在的又是什么范畴呢?

public class Company {
    private String id;
    private String name;
}
public class Employee {
    private String id;
    private String name;
}
public static Employee getSupervisor(Employee e1){...}
public static Company getWork(Employee e){...}
public static String getName(Employee e){...}

  我们不妨这样定义上面那段代码所描绘的范畴:这个范畴下至少有三个点:String, Employee, Company。而getSupervisor,getWork和getName则形成了这三个点之间的关联关系。
点:数据类型
  每种数据类型其实是一个集合,比如Integer这个类型就代表了整数的集合,而String则是字符串对象的集合。然而集合也可以是范畴中的一个点,就像螳螂动物这个范畴中的一个点一样。语言中可能预先定义了很多点,而我们定义的Employee和Company则形成了两个新的点。
箭头:函数
  函数Function(OO中的方法,可以转换为函数的形式,见第一篇)从数学的角度定义,就是两个集合之间的映射关系:A->B的函数是指A中的每个元素都能对应到B中的某个元素。但是在编程语言的范畴中,我们可以忽略A, B中的具体元素,转而直接将A和B作为范畴中的元素。
  那么,所有的函数就成为了具体的某一根箭头,如getName就成为了Employee->String的一根箭头,而getSupervisor则是Employee->Employee的箭头。
  如果函数有多个入参怎么办?比如add(int a, int b) 。正如函数签名所暗示的那样,我们可以把(a,b)看作是一个整体,将一个二元组映射为一个整数。因此,(a,b)二元组也是一种数据类型。

函数式编程所描绘的范畴

  大多数静态类型的语言都可以归入上面的范畴,但函数式编程的核心——函数组合,还没有体现出来。比如最常见的map(f: A->B, list: List[A]):List[B]。
  此处需要引入函子Functor的概念。

什么是Functor?

  如果两个范畴C和D,如果C范畴中的每个元素都能对应到D范畴中的某个元素(注意与函数一样,不要求是一对一关联,可以是多对一),并且C范畴中的箭头也能对应到D范畴中的箭头,那么这种范畴与范畴之间的映射就是一种函子Functor。
  比如在上海领导班子这个范畴中,有市委书记、市长、各区区长等元素;在江苏省领导班子这个范畴中,有省委书记、省长、各市市长等元素。那么上海领导班子中的市委书记就能映射为江苏省领导班子中的省委书记,而市长能映射为省长,说明了两个范畴之间的元素是可以映射的(此处是一对一关系);并且市委书记与市长的关系,在省委书记与省长之间依然成立,说明了范畴之间的箭头也是可以映射的。因此,这种映射的方式就是一个Functor。
  同样在这两个范畴中,如果将上海领导班子中的市长映射为江苏省领导班子中的市长,那么原先的一些关系在新范畴中将不能找到对应关系,因此不是每种映射都能成为一种Functor。

map与Functor

  如果add(int a, int b) = a+b,那么如果固定a不变,只变化b,结果会如何呢?比如在a=1的条件下,就变成了add(b) = 1+b。因此add(a,b)不仅可以看作是(a,b) -> int的映射,也能看作是a->(b->int)的一种映射。想像一下给定任何a=x,都会产生一个相应的新函数,这个函数的功能是将一个int+x。
  再看map(f: A->B, list: List[A]):List[B]这个签名,可以看作是(f, List[A])的二元组映射为List[B]。但如果换一个角度,则可以看作是给定一个f:A->B,得到一个新的函数mapf:List[A]->List[B]。
  我们已经知道,A和B作为两种类型是原范畴中的两个点,那么List[A]和List[B]呢?我们把它们看作是新范畴List中的两个类型List[A]和List[B]。而映射这两个范畴的方式,则是将X映射为List[X],那么以前在A,B之间存在的箭头都要能在List[A]和List[B]之间找到对应的箭头。这些箭头是范畴中元素之间的映射,而在编程语言中,就是A->B的函数。
  因此,假如给定任一f:A->B,经过map方法之后,如果都能产生一个全新的函数List[A]->List[B],那么原范畴中的每个箭头都能在新范畴中找到对应的映射。这就符合了Functor的定义。
  所以,map(A->B, List[A]):List[B]的每一种实现,都是从原范畴映射到List范畴的一个Functor。在Scala中,Functor的签名可以定义为这样的trait:

trait Functor[F[_]] {
    def map[A, B](f: A->B)(fa: F[A]): F[B]
}

高阶类型与函数式编程范畴

  像List[X]一样的数据类型在含有泛型或类型构造器的语言中比比皆是。因此,可以将泛型和高阶类型视为进入新范畴的一种方式。X经过Set[X]后就进入了Set这个范畴,经过Option[X]后就进入了Option这个范畴,经过Future[X]后就进入了Future这个范畴。(Set,Option,Future在函数式编程语言中都有相应的map方法)。
  但是List[X], Future[X]难道就不是原范畴中的类型了吗?它们确实属于原范畴。因此这些Functor是一种从原范畴映射到原范畴的一种Functor,称为自函子EndoFunctor。正如同有Employee->Employee的函数一样,也有从C范畴->C范畴的函子。只是将原范畴中的一部分类型映射为另一部分类型而已,同时仍然保持了映射后的关联关系。
  因此,函数式编程所描绘的范畴,是一个以数据类型为点,以函数为箭头,并且充满了自函子映射的范畴。
  而除了最基本的Functor之外,还存在Applicative Functor,Monadic Functor等特殊的Functor,分别以map2(fa: F[A], fb: F[B], f: (A,B)->C):F[C]和flatmap(fa:F[A], f: A->F[B]):F[B]为其特征,在Scala等函数式语言中可以产生更强的表现力。

个人观点

  从范畴论的角度看函数式编程,会产生一种全新的理解。OO之后人们以构建和封装对象的角度来看待编程,一度使编程与数学脱离。但函数式编程的出现,则让我们重新从数学理论的视角出发,将编程与数学又一次地联系在了一起。
  这种视角让我们更好地了解到OO语言与函数式语言之间设计哲学上的差异。尽管函数式编程的门槛更高,但由于其表现力、生产率和背后的数学基础,这类语言仍然值得我们投入更多的时间和精力去探索和学习。

猜你喜欢

转载自blog.csdn.net/samsai100/article/details/71512702
今日推荐