准备给TWU的同学做一个关于函数式的Session,昨天在准备Slides,顺手总结一下,有正在学习的小伙伴可以做一个参考。
What
Functional Programming(函数式编程)在概念上和Object Oriented Programming(面向对象编程), Procedural Programming(过程化编程)类似, 是一种编程范式。
与OOP以对象为中心的理念不同,FP将所有计算机的操作视为函数运算,函数是操作的基本单位。函数拥有和基本类型一样的地位,可以将一个变量赋值为函数(First class -- 一等公民)
,可以在函数的参数中传递函数(higher-order function -- 高阶函数)
。
Why
- 学习一点新的编程范式可以有效防止老年痴呆。
- 真的很有趣
- 相比于过程化、面向对象,函数式书写的代码更易读,更简短。
- 因为函数式编程是无
副作用(side effects)
的,不需要考虑死锁问题,适合并发编程,因此在云计算领域得到了广泛应用(Scala)
How
好了,进入正题
以下示例代码均为JavaScript
1. 副作用--Side Effects
先来看两段代码
//代码片段1
let minium = 20;
const checkAge = (age)=> age >= minium;
//代码片段2
let number = 2;
const multipleNumber = (n) => {
number = number * n;
return number;
}
这两段代码有问题吗?
通常情况下,代码片段1并不会发生什么问题, 我们传入年龄,并且判断是不是大于20岁。
但如果有人修改了minium
呢?此时判断的条件改变了,导致我们的结果也会改变。当我们第二次运行checkAge(22)
的时候,可能返回的并不是第一次运行的结果。
对于checkAge
这个函数来说,它需要观测的值不仅有入参age
,还有一个全局变量minium
,它的运行结果依赖系统状态,这对于程序员来说是十分痛苦的。
而代码片段2就很容易发现问题了,这个函数修改了一个全局变量,换言之,它修改了系统状态,当第二次输入相同参数的时候你会得到一个不一样的结果。
不,这太让人难过了,这不是我们想要的,我们希望我们的函数足够纯净,相同的输入永远得到相同的输出。而且,不要做多余的事:
偷偷在console里打一个log
偷偷给某个api发送一个request
偷偷修改本地文件系统
2. 纯函数--Pure Function
Side Effects好吗?我们心里都知道不好,但有的时候你不得不接受它,就像生活。
或许你的系统需要维护某些状态来运行不同的程序,和它们接触的函数就不可避免的变得不纯净起来。但这并不意味着我们就放弃治疗了,把怪兽关在壁橱里总比把它放出来到处搞破坏来得好。正所谓关注分离。
是时候引入纯函数了。
我们把上面的代码修改成这样如何:
//代码片段3
const checkAge = (age) => { const minium = 20; return age >= 20;}
//代码片段4
const multipleNumber = (number, n) => {return number * n};
对于代码片段3来说,现在它终于不用始终看着minium了,现在minium变成了它的一部分,永远不会改变。这样我们相同的输入,永远都会有相同的输出。
对于代码片段4,我们可能会有疑问:“原来我只用输入一个参数,现在我要输入两个参数了,有点不太对劲啊?”
“Emmmmm, 至少我们可以通过始终调用multipleNumber(2,n)
或者给number
一个默认值来让它变得纯函数起来const multipleNumber(n,number=2)
”
所以纯函数是什么呢?
Stateless(不改变状态,也不被状态影响)
3. 柯里化 -- Curry
让我们稍稍改变一下代码片段4,这次让我们消除疑虑。
//代码片段5
const multipleNumber = (number) => (n) => {return number * n}
“教练,有什么区别啊?我还是得传两个参数啊,唯一的区别是我得写两对括号了啊(multipleNumber(2)(n)
!!!”
哎呀别急,回头看看文章开头,"函数拥有和基本类型一样的地位,可以将一个变量赋值为函数(First class -- 一等公民)
,可以在函数的参数中传递函数(higher-order function -- 高阶函数)
。"
再看看代码片段5,看到两个=>
有没有想起什么?
我们有一整页的时间思考
……
……
……
……
……
……
……
……
……
……
……
……
……
……
……
……
……
……
……
……
……
……
……
……
……
……
……
……
……
……
……
……
……
……
……
答案是:
我们现在可以写这样的代码了:
const multipleTwo = multipleNumber(2); // (n) => {return 2*n}
const result = multipleTwo(3);//6
我们把这样拆分函数参数 -->
形成一个高阶函数 的过程,称为柯里化。
这就是一个最简单的柯里化的例子。
好处?
生成的高阶函数变成了一个工厂,用来生产诸如multipleTwo这样的函数。
有效消除重复代码,谁用谁知道。
再举一个例子:
const matchRegex = (regex, str) => str.match(regex);
将其柯里化之后得到一个叫matchRegex的工厂
const matchRegex = (regex) => (str) => str.match(regex);
我们可以生产这样的函数:
const hasSpace = match(/\s+\g);
对,matchRegex是没有上下文含义的,但生成的函数根据传入的正则产生了不同的业务含义。
*只传给函数一部分参数也叫局部调用,当然 和柯里化是完全不同的两个东西
4. 组合 -- Compose
现在相信我们已经对函数式编程有了一个初步的认知。
想象这样一个业务场景:
我们是Email Writer,
有时候我们需要把邮件的某些Text变成UPPERCASE,
const uppercase = (str) => str.toUpperCase();
有时候我们需要给Text末尾加上一个感叹号,!
const addBondToEnd = (str) => str+"!";
有时候我们需要既把Text变成uppercase, 也要在后面加一个感叹号:TEXT!
怎么办呢?
//So Easy
const addBondToEndAndUpperCase=(str)=>{
return addBondToEnd(uppercase(str));
};
把两个函数都调用一遍不就好了。我们还生成了一个新函数,每次想做这两个操作的时候调用这个新的函数就行了。
真棒!我们已经知道组合是什么东西了!
但多想一步,如果我们有许许多多这样的基础函数大概10000个
,我们想要生成不同的组合函数,我们还要这样手写新函数吗?
我太懒了,我不想这么做,重复写这样的code让我觉得生不如死。
让我们回到文章开头,"函数拥有和基本类型一样的地位,可以将一个变量赋值为函数(First class -- 一等公民)
,可以在函数的参数中传递函数(higher-order function -- 高阶函数)
。"
这次直接揭晓答案吧:
对于我这样的懒人,需要的是这样一个工具
const compose = (f, g) => (x) => f(g(x));
这样我就可以这样调用了:
const uppercase = (str) => str.toUpperCase();
const addBondToEnd = (str) => str+"!";
//addBondToEndAndUpperCase
compose(addBondToEnd, uppercase)("text");//TEXT!
//Of course you can assign it to a variable
const addBondToEndAndUpperCase = compose(addBondToEnd, uppercase)