有时候,一个关键字就是一扇通往新世界的大门。两年前,身边开始有人讨论函数式编程,拿关键字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 computer programming, a function may be considered a pure function if both of the following statements about the function hold:
The function always evaluates the same result value given the same argument value(s). The function result value cannot depend on any hidden information or state that may change while program execution proceeds or between different executions of the program, nor can it depend on any external input from I/O devices (usually—see below).
Evaluation of the result does not cause any semantically observable side effect or output, such as mutation of mutable objects or output to I/O devices (usually—see below).
纯函数即为函数式编程所强调的函数,上述两点可翻译为:
不依赖外部状态
不改变外部状态
所以对函数式编程当中函数的理解,最后还是落实到状态的理解。静止的状态是安全的,变化的状态是危险的,之所以危险可以从两个维度去理解,时间和空间。
时间
变量一旦有了状态,它就有可能随着时间而发生变化,时间是最不可预知的因素,时间会将我们引至什么样的远方不得而知,我们每创造一个变量,真正控制它的不是我们,是时间。
时间的武器是变化,是赋值,赋予变量新的值,在不可预知的未来埋下隐患。
- (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,也是颇具函数式编程精髓的。
一等公民(First Class)
当我们把函数改造成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君觉得对于新接触的知识,我们更应该关注其诞生的目的,及其背后隐含的思想,抓住了本质,理解那些令人望而生畏的技术术语就更有底气了。
欢迎关注公众号:MrPeakTech