最近在学习函数式编程,记录了一些笔记,也总结了一些自己的理解。我准备整理一下陆陆续续发出来,本文不算是一篇文章吧,算是自己在学习函数式编程中的一些总结,也算一个引子。
一些约束
- 不要为了延迟执行,而简单地用一个函数把另一个函数包起来。
- 参数命名的时候,不要把参数名限制在特定的数据上,容易造成重复造轮子。
- 函数不依赖外部值
纯函数
1.概念
函数式编程中函数是一等公民,即普通人。对于函数,强调纯函数的概念。什么是纯函数呢?
纯函数是这样一种函数,相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用。
比较明显的一个例子就是slice和splice。前者纯而后者不纯,想想为什么~
这里说的没有副作用具体是什么呢?像上面我提到的splice,它的副作用就是修改了本来的数据,还有一些容易想到的情况,如向数据库插入了数据,打印了数据,获取了用户输入等。 总之,就是一切跟函数外部环境反生交互的行为。
归根结底,这些行为很容易导致“相同的输入返回相同的结果”这一概念的失效。这也是我们尽量避开它们的原因。
2.为什么追求纯?
为什么要花这么多力气去实现纯函数呢,可见的几点好处如下:
可缓存性(Cacheable)
重复的计算不需要多次计算,这就是可缓存性。
let addFive = memoize(x=>x+5)
addFive(1);
addFive(1);
addFive(1);
//真正的计算只会发生一次。
memoize的实现很简单,使用一个对象来存储计算过的值即可。下面是一个简单的实现
const momize=(func)=>{
//cache对象用于存储计算过的值
let cache={}
return ()=>{
let key = JSON.stringify(arguments)
if(!cache[key]){
cache[key] = func.apply(this,arguments)
}
return cache[key]
}
}
可移植性(Portable)
纯函数的依赖很明确,需要的数据都在参数中体现了。这样做,使应用更加灵活。因为一切的依赖都参数化了,当依赖变化时,直接把新的依赖传递进去就好了。
可测试性(Testable)
这也是可以预见的,相同的输入具有相同的输出。意味着测试时我们只需要给函数一个输入,然后断言它的输出即可。
引用透明(referential transparency)
函数的返回值只依赖于它的输入,这就是引用透明性。很明显,纯函数具有这个特性。这个特性可以帮助我们更好的分析我们的程序。
并行
纯函数可以任意并行的运行。因为纯函数没有副作用,不会和其它纯函数进入竞争状态。也不需要访问共享的内存。
柯里化
对于函数式编程来说,柯里化是一个不可或缺的工具。它的概念很简单,只传递给函数一部分参数来调用它,返回一个接受剩下参数的函数。
一个被举得最多的例子:
const addFunc =(a,b,c,d)=>{
return a+b+c+d;
}
const add = curry(addFunc)
add(1)(2)(3)(4) //10
add(1,2)(3,4) //10
add(1,2,3,4) //10
经过柯里化,现在我们不需要再一次性的将所有参数传入了。
代码组合(compose)
之前我有一篇文章分析过redux的源码,其中有一个文件compose.js:
export default function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
这就是组合函数,一个函数的返回将作为另一个函数的输入。
PS:这个代码是从左往右运行的,函数式推荐从右向左运行。据说这样更加能够反映数学上的含义。
通过组合,我们可以像搭积木一样把简单的功能拼凑成复杂的功能。
pointfree
这是一种风格,指的是数据无关,就像上面的compose,举个例子:
//非pointfree 风格
let upperReplace=(arg)=>{
return arg.toUpperCase().replace(/\s+/ig/,'_')
}
//pointfree风格
let upperReplace = compose(replace(/\s+/ig/,'_'),toUpperCase)
非常明显,非pointfree模式提到了数据arg,而pointfree没有。可以看出pointfree还能帮我们减少不必要的命名,我相信你们也和我一样觉得命名是一件头疼的事。
debug
使用组合有时容易出错,比如参数个数对应不上之类的。对于组合有一种比较实用但不纯的方法来追踪代码的执行情况。
let trace = curry((tag,x)=>{
console.log(tag,x);
return x;
})
//将trace放在合适的地方,就可以看到该处的日志,从而纠错
let test = compose(otherOper,trace("after toUpper"),toUpper,firstEle,reverse)