有时候,一个关键字就是一扇通往新世界的大门。两年前,身边开始有人讨论函数式编程,拿关键字 Functional Programming 一搜,全是新鲜的概念和知识,顺藤摸瓜,看到的技术文章和框架也越来越多。
我有个习惯,在接收新知识的时候,我都会用已有的知识去做对比,我更关注新事物能对现有产品和知识体系带来哪些好处。
计算机发展到今天,已经很久很久没有理论层面的升级了,现今绝大部分新的知识都是基于已有的内核做包装。函数式编程不是新生事物,也不是独立的知识孤岛,函数式编程核心思想离我们也没那么远。
这篇文章会抛开陌生的术语和概念,只站在抽象和基础的层面,去聊下函数式编程和我们现有编程习惯的联系。
Functional Programming 翻译为函数式编程,初次接触的时候会不由自主的认为,这种编程范式的核心在于对 Functional 的理解,或者说是对函数的理解。函数我们每天都在写,还有什么需要特别去理解的吗?我个人觉得这是个误区,相对于理解「函数」,我们更需要理解的其实是「状态」。如果叫做 Stateless Functional Programming 可能会更贴切一点。
函数和状态是人人都熟悉的概念,可越是简单的每日可见的概念,越难理解的透彻。说到状态,很多同学会联想到变量,局部变量,全局变量,property,model,这些都可以成为状态,但变量和状态又不是一回事,要真正理解状态,得先理解下面一行代码:
- int i = 0
简单的一行代码,分析起来却有不少门道。
「 i 」就是我们所说的变量,一个变量可以看做是一个实体,真实存在于内存空间的实体。int 是它的类型信息,是对于它的一种约束,类似于上帝对于人类性别的约束,每个人都需要有性别。0 是它被赋予的一个值,值是外部信息,类似于人的职业,职业不是与生俱来的,人一生可以选择从事不同的职业。
变量是我们要分析的目标,它的类型信息,值信息虽然会约束变量的行为,但不是我们关注的重点,真正让变量变得 危险 的是中间的等号, = 是个赋值操作,意味着改变 i 的值,原本处于静态的 i,由于一个 = 发生了变化,它的值发生了变化,它可以变为 1,或者 10000,或者其他任何值,这个看似简单的改变可以说是我们程序的 bug 之源,值的变化可以像扔进湖面的石头,层层叠叠影响其他空间和实体。
一旦一个变量开始与 = 打交道,一旦变量的值会发生变化,我们就可以说这个变量有了 状态 。或者我们可以说,有 = 就有状态。
状态也是个相对的概念,变量都有其生命周期,一旦变量被回收,其所包含的状态也随之消失,所以状态所带来的影响是受限于变量的生命周期的。我们看下这段代码:
- - (int) doNothing {
- int i = 0;
- return i;
- }
i 是函数 doNothing 内部的临时变量,分配在内存的栈上,一旦 return,i 的生命周期也随之结束。
站在 doNothing 函数内部这个空间范畴来说,i 是有状态的,i 被赋予了值 0,当 renturn 执行之后,i 被内存回收了,i 随之消失,其所对应的状态也消失了,所以一旦出了 doNothing,i 又变得没有状态了。代码虽然执行了 return i,但返回的其实是 i 所代表的值,i 将自己的值交出来之后,就完成了自己的使命。
所以站在 doNothing 函数外部空间的角度来说,doNothing 的使用者是感受不到 i 的存在的,doNothing 的调用方可以认为 doNothing 是无状态(stateless)的,无状态意味着静止,静止的事物都是安全的,飞驰而过的火车和静止的石块,当然是后者感觉更安全。
我们编写代码的时候会经常谈论状态,函数的状态,类的状态,App 的状态,归根结底,我们所讨论的是:在某个空间范畴内会发生变化的变量。
函数式编程当中的函数 f(x) 强调无状态,其实是强调将状态锁定在函数的内部,一个函数它不依赖于任何外部的状态,只依赖于它的入参的值,一旦值确定,这个函数所返回的结果就是确定的。可能有人会觉得入参也是状态,是外部传入的状态,其实不然,我前面说过变量才会有状态,值是没有状态的,入参传入的只是值,而不是变量。下面两个函数,一个入参是传值,一个入参是传变量:
- - (void) doNothing: (int) v //传值
- {
- } - (void) doNothing: (NSMutableArray * ) arr //传变量
- {
- }
第二个版本的 doNothing,不但是传入了变量,还是可以变化的变量,是真正意义上的外部状态。很有可能在你遍历这个 arr 的时候,外部某个同时执行的线程正在尝试改变这个 arr 里的元素,是不是很危险?
所以对于下面两种调用来说:
- [self doNothing: user.userID]; [self doNothing: user.friends];
第一个调用只是传入了 userID 所对应的值,第二个调用却传入了 friends 这个变量实体。第一个没依赖,第二个有依赖,第一个没状态,对调用方来说是安全的,对整个 app 来说也是安全的,既避免了依赖外部的状态,也不会修改外部的状态,即:不会产生 side effect,没有副作用。
所以让我来总结函数式编程当中的函数,可以一句话归结为: 隔绝一切外部状态,传入值,输出值 。
我们再来看看函数式编程当中的纯函数(Pure Function)的定义:
In , a may be considered a pure function if both of the following statements about the function hold:
纯函数即为函数式编程所强调的函数,上述两点可翻译为:
所以对函数式编程当中函数的理解,最后还是落实到状态的理解。静止的状态是安全的,变化的状态是危险的,之所以危险可以从两个维度去理解,时间和空间。
变量一旦有了状态,它就有可能随着时间而发生变化,时间是最不可预知的因素,时间会将我们引至什么样的远方不得而知,我们每创造一个变量,真正控制它的不是我们,是时间。
时间的武器是变化,是赋值,赋予变量新的值,在不可预知的未来埋下隐患。
- - (void) setUserName: (NSString * ) name {
- //before assignment
- _userName = name;
- //after assignment
- }
一旦有了赋值操作,时间就找到了空隙,可以对我们代码的执行产生影响。或许是在此刻,或许是明天,或许是在 appDidFinishLaunch,或许是在 didReceiveMemoryWarning。每一个赋值操作都是一颗种子,可以结出新 feature 或者新 bug。
变量会随着时间变化,有状态的函数也会随着时间的流动产生不同的输出,Pure Function 却是对时间免疫的,纯函数没有状态,无论站在多长的时间跨度去执行一个纯函数,它所输出的结果永远不会变,从这一角度看,纯函数处于永恒的静止状态。
如果把一个线程看成一个独立的空间,在程序的世界当中,空间会产生交叉重叠。一个变量如果可以被两个线程同时访问,它的值如果可以在两个空间发生变化,这个变量同样变得很危险。
Pure Function 同样是对空间免疫的,无论多少个线程同时执行一个纯函数,纯函数总是产生相同的输出,而且不会对外部环境产生任何干扰。
多线程的 bug 调试起来非常困难,因为我们的大脑并不擅长多路并发的思考方式,而函数式编程可以帮我们解决这一痛点,每一个纯函数都是线程安全的。
函数式编程通过 Pure Function,使得我们的代码经得起时间和空间的考验。
我们可以把一个 App 的代码按照函数式编程的方式,打散成一个个合格的 pure function,再通过某种方式串联起来,要方便的串联函数,需要把函数变为一等公民,需要能像使用变量一样方便的使用函数。
一个 Pure Function 可以是 stateless 的,但我们的 App 可以变成 stateless 吗?显然不能。
离开了变量和状态,我们很难完整的描述业务。用户购物车里的商品总是会发生变化,今天或明天,我们总是需要在一个地方接收这种变化,保存这种变化,继而反应这种变化。所以,大多数时候,我们离不开状态,但我们能做的是,将一定会变化的状态,锁定在尽可能小的时间和空间跨度之内,通过改变代码的组织方式或架构,将必须改变的难以管教的状态,囚禁在特定的模块代码之中,让不可控变得尽量可控。
其实,即使不严格遵从函数式编程,我们同样可以写出带有 Functional Programming 精髓的代码,一切的一切,都是对于状态(state)的理解。
在我看来,NSMutableArray 的 copy,也是颇具函数式编程精髓的。
当我们把函数改造成 pure function 之后,会产生一些奇妙的化学连锁反应,这些反应甚至会改变我们的编程习惯。
一旦我们有了绝对安全的纯函数,我们当然希望能尽最大可能的去发挥它的价值,增加它出现和被使用的场景。为了加大纯函数的使用率,我们需要在语言层面做一些改造或者增强,以提高纯函数传递性。怎么增强呢?答案是 将函数变为一等公民 。
当我们的变量可以指向函数时,这个变量就有了函数的身份。当我们把这个变量当做函数的参数传入,或者函数的返回值传出的时候,这个变量就有了自由迁徙的能力。
一个函数 A,可以接收另一个函数 B 作为参数,然后再返回第三个函数 C 作为返回值。类似下面的一段 swift 代码:
- func funcA(funcB: @escaping(Int) - >Int) - >(Int) - >Int {
- return {
- input in
- return funcB(input)
- } //funcC
- }
在 funcA 的定义里,funcB 是作为参数传入,funcC(匿名的)是作为返回值返回。funcB 和 funcC 在这个语境里就称之为 first class function 。而 funcA 作为 funcB 和 funcC 的管理者,有个更高端的称谓: high order function 。
有了 first class function 和 high order function,我们还会收获另一个成果: 语言的表达力更灵活,更简洁,更强大了。 举个例子,我们写一段代码来实现一个功能:参加 party 前选一件衣服。用传统的方式来写:
- func chooseColor(gender: Int) - >Int {
- return 0
- }
- func dressup(dressColor: Int) - >Int {
- return 1
- }
- //imperative
- let dressColor = chooseColor(gender: 1) let dress = dressup(dressColor: dressColor) user.dress = dress
先定义函数,再分三步依次调用 chooseColor, dressup,然后赋值。
如果用 first class function 和 high order function 的方式来写就是:
- func gotoParty(dressup: @escaping(Int) - >Int, chooseColor: @escaping(Int) - >Int) - >(Int) - >Int {
- return {
- gender in let dressColor = chooseColor(gender) return dressup(dressColor)
- }
- }
- //declarative
- let prepare = gotoParty(dressup: {
- color in
- return 1
- },
- chooseColor: {
- gender in
- return 0
- }) user.dress = prepare(1)
gotoParty 函数柔和了 dressup 和 chooseColor,gotoParty 成了一个 high order function,当我们读 gotoParty 的代码的时候,这单一一个函数就将我们的目的和结果都表明了。
这就是 high order function 的神奇之处,原先啰啰嗦嗦的几句话变成一句话就说清楚了,它更接近我们自然语言的表达方式,比如 gotoParty 可以这样阅读:我要挑选一件颜色适合的衣服去参加 party,这样的代码是不是语意更简洁更美呢?
注意,functional programming 并不会减少我们的代码量,它改变的只是我们书写代码的方式。
这种更为强大的表达力我们也有个行话来称呼它: declarative programming 。而我们传统的代码表达方式(OOP 当中所使用的方式)则叫做: imperative programming 。
imperative programming 更强调实现的步骤,而 declarative programming 则重在表达我们想要的结果。这句话理解起来可能有些抽象,实在理解不了也没啥关系,只要记住 declarative programming 能更简洁精炼的表达我们想要的结果即可。
以上都是我们将 function 变为一等公民所产生的结果,这一改变还有更多的妙用,比如 lazy evaluation 。
上述代码中的 dressup 和 chooseColor 虽然都是 function,但是他们在传入 gotoParty 的时候并不会立马执行(evaluation),而是等 gotoParty 被执行的时候再一起执行。这也很大程度上增强了我们的表达能力,dressup 和 chooseColor 都具备了 lazy evaluation 的属性,可以被拼装,被 delay,最后在某一时刻才被执行。
所以,functional programming 改变了我们使用函数的方式,之前使用 OOP,我们对于怎么处理变量(定义变量,修改值,传递值,等)轻车熟路,到了函数式编程的世界,我们要学会如何同函数打交道了,要能像使用变量一样灵活自如的使用函数,这在刚开始的时候确实需要一段适应期。
函数式编程近几年颇受技术圈的关注,Peak 君觉得对于新接触的知识,我们更应该关注其诞生的 目的 ,及其背后隐含的 思想 ,抓住了本质,理解那些令人望而生畏的技术术语就更有底气了。
来源: http://www.tuicool.com/articles/36BjIfu