2017年我写过一篇文档关于函数式编程,那是主要用的还是OC 语言。6年过去了再看函数式编程感觉当时还是青涩。 《iOS 面向函数编程的理解》
最初接触函数式编程还是Rx 系列响应式的概念带来的,这么多年用过Rxswift,Rxjs ,一直理解不够深刻。
React 带来的hooks, 官方概念是利用函数式编程方式,更好的组合,开发和测试。但是还是觉得不够深刻,又看了些资料,梳理下自己的理解,重点关注react 中的提现。
概念
函数式编程是种编程方式,它将电脑运算视为函数的计算。函数编程语言最重要的基础是λ演算(lambda calculus),而且λ演算的函数可以接受函数当作输入(参数)和输出(返回值)。
除了函数式编程方式,还有:
- 面向对象编程
- 面向过程编程
- 命令式编程
纯函数
纯函数是指在函数的执行过程中,不会对程序的状态进行任何改变,也不会对外部环境产生任何副作用,即只依赖于其输入参数,而不依赖于任何外部变量或状态的函数。
纯函数的特征
1、相同的输入总是产生相同的输出,即函数的输出只由输入决定,不受外部状态或副作用的影响。
2、函数对外部状态没有依赖,也不会改变外部状态,即不会对程序的其他部分产生任何副作用。
3、函数不会修改传入的参数,而是返回一个新的值,保持输入参数的不可变性。
4、函数的执行过程对于调用者来说是透明的,即调用者不需要了解函数的内部实现细节,只需要关注输入和输出。
举例
纯函数:
js
function sum(a,b) {
return a + b;
}
非纯函数(引入外部变量):
js
var c = 0
function sum(a,b) {
return a + b + c;
}
非纯函数(副作用):
js
function sum(a,b) {
console.log(a);
return a + b ;
}
纯函数的优势
- 可靠,输出是由输入决定
- 可测试性强
- 可缓存
- 没有副作用,利于代码维护重构以及并行处理
JS 中函数编程思想应用
函数编程思想,函数是一等公民,输入沿着函数管道组合产生想要的结果。
下面这个例子利用map、filter、reduce 等函数对入参一个对象数组进行加工,这是一个简单的函数式编程的思想应用,array 每次经过纯函数的加工,返回结果作为输出再次加工。
js
let arr = [{key: 'a', value: 1},{key: 'b', value: 2},,{key: 'c', value: 3}]
arr.map((item)=>item.value).filter((item)=>item !==2).reduce((t,i)=>t+i,0)
一个输入到达终点的路径很多,路径都是一个个函数。所以函数之间的调用关系也是非常重要的优化手段。
函数柯里化
定义:把接收多个参数的函数,转成接收单一参数的函数,并且返回余下参数的函数的过程就叫做函数柯里化
举例
js
// 正常函数
function add(a, b){
return a + b
}
add(1, 2)
// 转成柯里化函数
function add(a){
return function(b){
return a + b
}
}
add(1)(2)
为什么要柯里化
存在即合理,柯里化的使用场景是哪些呢?
先简单的变化一下上面的例子:
js
// 正常函数
function add(a, b){
return a + b
}
add(1, 2)
// 转成柯里化函数
function add(a){
return function(b){
return a + b
}
}
let f1 = add(1)
let add2 = f1(2)
let add3 = f1(3)
这样大概可以看出来柯里化的意义,但是场景不是很合理。下面是一个正则的例子:
js
// 正常正则验证字符串 reg.test(txt)
// 函数封装后
function check(reg, txt) {
return reg.test(txt)
}
// 即使是相同的正则表达式,也需要重新传递一次
console.log(check(/\d+/g, 'test1')); // true
console.log(check(/\d+/g, 'testtest')); // false
console.log(check(/[a-z]+/g, 'test')); // true
// Currying后
function curryingCheck(reg) {
return function (txt) {
return reg.test(txt)
}
}
// 正则表达式通过闭包保存了起来
var hasNumber = curryingCheck(/\d+/g)
var hasLetter = curryingCheck(/[a-z]+/g)
console.log(hasNumber('test1')); // true
console.log(hasNumber('testtest')); // false
console.log(hasLetter('21212')); // false
上面的示例是一个正则的校验,正常来说直接调用 check 函数就可以了,但是如果我有很多地方都要校验是否有数字,其实就是需要将第一个参数 reg 进行复用,这样别的地方就能够直接调用 hasNumber、hasLetter 等函数,让参数能够复用,调用起来也更方便。
柯里化封装
js
// 支持多参数传递
function currying(fn, ...args) {
var self = this
var len = fn.length;
var args = args || [];
return function() {
var _args = Array.prototype.slice.call(arguments);
Array.prototype.push.apply(args, _args);
// 如果参数个数小于最初的fn.length,则递归调用,继续收集参数
if (_args.length < len) {
return currying.call(self, fn, _args);
}
// 参数收集完毕,则执行fn
return fn.apply(this, _args);
}
}
compose 函数
compose 函数可以接收多个独立的函数作为参数,然后将这些函数进行组合串联,最终返回一个"组合函数"。
compose 函数的实现:
js
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)))
}
举例说明:
js
const add = (a) => a + 1
const mul = (a) => a * 10
const f = compose(add, mul)
console.log(f(1))
结果: 11
这个无需多解释,就是把函数由右向左
执行,
pipe 函数
js
function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduceRight((a, b) => (...args) => a(b(...args)))
}
举例说明:
js
const add = (a) => a + 1
const mul = (a) => a * 10
const f = compose(add, mul)
console.log(f(1))
结果: 20
我理解,柯里化和组合、pipe 应该会结合理解使用。