编程经典案例之函数
《Composing Software》一书有一章简单回顾了函数式编程的历史。
当然,这避不开要提到 1936 年阿伦佐·丘奇和阿兰·图灵各自独立地创造了第一批通用计算机的抽象设计。
图灵的计算模型以及他对于求解的问题本质的概念是最靠近物理的。所以,我们目前建造的计算机都是基于图灵的计算模型。以及,同样以靠近物理为哲学的汇编语言,C 语言都是这样发展出来的。
而丘奇的 Lambda 运算,则是以函数为基础的另一种计算模型,也是函数式编程语言共同的祖先。
虽然在数学上能证明 2 者是等价的,但并不代表,也没必要非得要在其中进行 2 选 1。
比如,Lambda 运算的核心是 Lambda 表达式,以此形成函数定义、函数应用和递归的形式系统。这很自然,因为函数本来就是抽象的,如果用靠近物理的表达,反而不直观。
但是,当使用 Lambda 表达式定义出布尔值、数值和各种基本操作符等语言元素后,虽然能够形成纯粹的函数式编程语言,却反而没有靠近物理的表达来的直接。
纯函数本身很好理解,就像数学的函数一样,固定的输入返回固定的输出,好处就是:永远不会改变,很确定。
带有副作用的函数也容易理解,坏处就是不确定。
究其原因,Scala的作者总结的很精辟:「非确定性=并行处理+可变状态」
然而,副作用不可避免,要做的就是把副作用函数限定在一个小范围内,能改为纯函数的地方都改造为纯函数。
如这个常见的例子,往数组中添加一项:
靠近物理的写法是,就地修改。数据只有一份,被多个操作逻辑共享。
接下来介绍一个纯函数改造方法:由就地修改改为创建新的副本。修改后返回新的副本,不同的逻辑处理各自的副本。
这样,由纯函数组合而成的逻辑方便测试,不易产生bug。
用这个方法足以应付一般的搬砖。
提到函数式编程,大家应该都听过函数式更具声明式(相比命令式而言),意思是函数式更注重表达的是做什么而不是怎么做。
如果你学编程是从 C、Java 这些不支持函数式的语言入门,在刚接触函数式时,需要做一些思想转换。
因为使用 2 者时,有些选择是刚好相反的,但归结起来,主要还是函数式更偏爱声明性。
其他的差别,如避免共享状态、避免可变状态、避免副作用都可以包含在声明性里面。
如上面这个例子,就是命令式的,里面自然用到了可变状态,如果将内部的临时变量放到函数外面,则就用到了共享状态和副作用。
下面的例子,则是声明式的,几乎全部的操作细节都被隐藏在 map 函数里。
由于 map 是标准操作,可以最大限度地复用,熟悉之后,理解起来几乎不费力。
最后,就只剩和 map 组合的匿名函数了,因为这个是必须要理解的,所以声明式可以将理解难度降到最低。
2 者的区别,也可以类比语音聊天和文字聊天的区别,一个感性,凭直觉,效率高;一个理性,要思考,效率低。
另一个常用的高阶函数是 filter,也就是过滤,可以用来消除判断语句,如 if 。
看下面两个用到 if 的常用例子,在数据处理流中添加各种判断逻辑。一个是过滤数据长度,另一个过滤以 s 开头的数据。
对比两个例子,能明显看出除了判断逻辑不同外,其他代码都是一样的。
如何重用这部分代码呢?可以把这部分代码当做模版,封装成高阶函数。
例子中的代码是处理数组的,当然已经有封装好的高阶函数可以使用,就是 filter。
如果要自己封装,可以看下面的例子。
遍历一个数组,然后将处理的结果再收集起来,这种模版叫做 reducer。filter 就是一种 reducer,将过滤和 reducer 模版组合在一起。
再看另外一个例子,过滤大于某个数的数据。
可以看出,表达简洁,没有一丝多余。
也可以这样类比,方便理解:高阶函数像个领导者,只负责安排任务;一阶函数,也就是普通函数则是干活的,需要执行具体的步骤。
接下来要介绍一个特殊的高阶函数用法。它的名字就不在这里说了,因为这不是重点。
这个特殊的用法,其实就是一种套路,如果你喜欢上它,很有可能就退不回去了。
我们先看下普通的函数定义,不管各种语法糖如何变化,本质上是一样的,都是由参数列表和返回值组成的签名二元组。
再看下这个套路,如果你对函数的定义理解不够的话,看到这样的写法,会引起不适反应。
但再看下它的用法,也感觉怪怪的。但这不是它常用的用法。
再看下这两个例子,感觉就会好多了。
从用法来看,它们像是遵从了一些约定的函数组合方法。
特点是每个函数都只有一个参数。有了这个约束,虽然自由度上少了一些,但更加强调对函数组合的设计,能把一个函数只做一件事这个原则执行得更彻底。
之前我们遇到的函数组合,一般情况是,只有两三个函数,然后自己写个函数把它们组合起来,这样很方便,简单易懂。
但是当函数较多时,尤其不同的函数组合,会让新产生的函数数量爆增,达到难以维护的地步。
如果刚才的情况你能忍受,那压倒骆驼的最后一根稻草就是:多个函数的组合,其中还包含了逻辑,比如增加一个判断逻辑,情况A选用函数F,情况B选G,这些逻辑是依据运行时的输入来判断的。
这时,就需要能动态组合函数的工具出现了。
然后,刚才的手工组合就可以写成这样。
如果要增加更多函数进来,比如打印一些调试信息,也很方便。
函数的主体保持不变,只需调整组合函数的参数即可。
可以看出,这个用来组合函数的函数就很通用,减少了没必要临时函数。结构上也达到了最精简。
版权声明: 本文为 InfoQ 作者【顿晓】的原创文章。
原文链接:【http://xie.infoq.cn/article/77bb80d880c2337b36f21e4e5】。文章转载请联系作者。
评论