函数式编程的思想来源与具体表现

目录

●知识的互通

●从范畴论说起

●函子

●MapReduce

●小结


●知识的互通

笔者最近在学习的过程中,接触到了MapReduce、JavaScript的高阶函数、范畴论等相关知识。突然想到几个月前写过JAVA8函数式编程的相关博文,发现其实知识是互通,其思想都是相同的,只不过具体表现形式不一样。今天抽空回顾加总结一下这方面的内容,作为函数式编程学习的心得体会。

●从范畴论说起

笔者在《Java8新特性之Stream初探》中第一次提到了函数式编程的概念,感觉还可以继续深挖,把它的来龙去脉再解释清楚一些。

首先,我们从范畴论(Category Theory)开始说起。它指的是“事物及其它们之间的联系”,抽象地看来就是下图这样:

注意,这里的事物是任意的事物,联系也是任意的联系,下图也是一个范畴,左边的事物是语言,右边的事物是分类,联系是解释型语言/编译型语言:

函数式编程的思想来源与具体表现_第1张图片

下图同样是一个范畴,将动植物和颜色进行了联系:

函数式编程的思想来源与具体表现_第2张图片

我们把图中的事物称为范畴成员,联系的箭头表示范畴成员之间的关系,称为"态射"(morphism)。通过"态射",一个成员可以变化(transformation)为另一个成员。

我们把它继续抽象一下,可以将范畴认为是值的集合+函数,即集合中的某一个值能通过函数转化为另一个值。或者,换句话说,我们可以把范畴认为是一个容器,它里面包括值的集合+函数。

如果把这套理论用到编程中,自然就表现成了函数式编程:一个广义的值,通过函数,可以转化为另一个广义的值。这里说的广义的值指的可以是一个值、多个值的集合、一个范畴甚至多个范畴的集合

函数式编程中很重要的思想就是通过函数进行变化,说得夸张点,代码中除了IO读写这种操作外,应该每一步都是运算/变化,每一步都有返回值!

●函子

正如上一部分提到的,范畴内值可以变化,范畴间也可以变化。让范畴的值发生变化的东西我们成为态射,而让范畴间发生变化的东西我们则称为函子。我们结合下图来解释:

函数式编程的思想来源与具体表现_第3张图片

图中左右各为一个范畴。左边范畴中f是一个态射,右边范畴中Ff也是一个态射。右边绿色箭头表示的正是函子,它能把左边的值映射/转变为右边的值,并且,它还能把左边的态射映射/转变为右边的态射!是不是很神奇?

通常,在函数式编程的世界里,我们把这种范畴间的映射称之为函子。为了使值的集合转化为一个范畴,我们通常会使用一个of()之类的方法,将其放入容器中,和函数/态射共同组成范畴。对的!是不是很熟悉?JAVA8中Optional类就是这样的一个范畴,它提供了of()方法将值的集合包装了起来!它具备自身值的转化(态射),也具备范畴间的转化(函子)。笔者学习到这个地方的时候也是感觉豁然开朗。

也许你会说Optional类还有一个ofNullable()方法将值的集合进行包装,Bingo!这个方法构建出的范畴在范畴论中,具备的函子称之为Maybe函子,即它的映射方法中提供了空值校验

有一段话说的很形象——

函数式编程里面的运算,都是通过函子完成,即运算不直接针对值,而是针对这个值的容器----函子。函子本身具有对外接口(map方法),各种函数就是运算符,通过接口接入容器,引发容器里面的值的变形。

因此,学习函数式编程,实际上就是学习函子的各种运算。由于可以把运算方法封装在函子里面,所以又衍生出各种不同类型的函子,有多少种运算,就有多少种函子。函数式编程就变成了运用不同的函子,解决实际问题。

我们回顾一下,函数式编程的世界中,可以把值传给一个函数,甚至还可以把函数本身传给一个函数(想想JAVA8中的lambda表达式吧)。需要注意的是,无论是态射还是函子,它们所做的仅仅是转化,而不是改变原本的值,换句话说原本的值是存在的,不会修改它。例如我们把一个对象传入函数,这个函数的作用应该是返回一个新的对象,而不是去修改原本的对象。虽然我们可以定义一个函数完成此功能,但是在函数式编程的世界中,这是不标准的,应该摒弃。自此,这一部分的数学原理(仅说范畴论这一方面)解释得差不多了。

●MapReduce

在《Java8新特性之Stream初探》中,笔者提到了Map和Reduce函数,从上一部分我们知道Map函数其实就是范畴论中的函子,那么Reduce又是什么呢?这得从2003年至2004年间自谷歌的几篇论文说起。

论文中首次提出了MapReduce——这种用来进行分布式运算的解决方案。我们结合下图来说:

函数式编程的思想来源与具体表现_第4张图片

①外部传来一个任务需要处理/运算,传给调度主机(如果没有分布式,那就是调度主机一台机器进行处理/运算然后输出结束了);

②调度主机指定Map集群和Reduce集群;

③输入的内容以K/V的形式分发到Map集群,进在集群中进行映射/转化处理;

④映射/转化处理的中间值存在Map集群中的本地磁盘上,依旧是K/V的形式;

⑤将中间值传给Reduce集群,它做的工作是将相同K值对应的V值集合进行处理,例如合并;

⑥最终,从Reduce集群产生输出。

我们可以看到,Map做的是一个映射与转化的工作,得到一个范畴;而Reduce做的这是把这个范畴里的集合进行浓缩与合并

这个思想虽然来源于分布式计算,但几经发展,最终变成了函数式编程中的重要特征。我们可以从代码上看出,例如JavaScript中,数字arr先通过map函数进行一一映射,映射的计算规则是pow,即原值经平方运算得到新值,这一步后得到[1,9,4,16,1];然后通过reduce函数进行整体的合并/浓缩/归一/稀释(无论你怎么称呼它),总之是从集合中第一个值开始,与后一个值进行运算,运算的规则是deal,即将前一个值与后一个值的绝对值之差乘以二作为新的值,直到计算完,最终结果result是30。

var arr = [1,3,2,4,1];

function pow(x) {
    return x * x;
}

function deal(x,y){
    return Math.abs(x-y)*2;
}

var result = arr.map(pow).reduce(deal);

console.log(result);

JAVA8中同样也具有相同的内容,我们用Lambda表达式来写一下:

public void mapReduceTest(){
    List integers = new ArrayList<>();
    integers.add(1);
    integers.add(3);
    integers.add(2);
    integers.add(4);
    integers.add(1);

    Integer result = integers.stream().map(i -> i*i).reduce((x,y) -> Math.abs(x-y)*2).get();
    System.out.println(result);
}

最终结果同样是30。做法依旧是把值的集合List包装成范畴,利用.stream(),然后进行map一一映射,最后进行reduce浓缩。

其中map与reduce接口的源码如下,都是函数式接口:

 Stream map(Function mapper);

Optional reduce(BinaryOperator accumulator);

●小结

我们再用一句话总结下函数式编程的思想:你只需要把事情做完,而为了把事情做完,你其实不需要东西,你需要的是动作。也就是说,你不需要名词,你需要的是动词。函数式编程相比之前的命令式编程,能让我们的代码看上去更整洁,阅读理解起来也更接近自然语言。那么,为什么不尝试学习并使用它呢?

最后,给大家推荐下阮一峰老师的两篇博文《函数式编程入门教程》、《函数式编程初探》,笔者学习和写作过程中参考了其不少内容,深受启发。

你可能感兴趣的:(JAVA)