函数式编程(二)
纯函数
函数式编程中的函数,指的就是纯函数,这也是整个函数式编程的核心
纯函数:相同的输入永远会得到相同的输出,而且没有任何可观察的副作用。
纯函数就类似数学中的函数(用来描述输入和输出之间的关系),y = f(x)
绿色的就是对函数的输入,蓝色的就是对函数输出,f就是函数,就是输入输出的关系
lodash 是一个纯函数的功能库,提供了对数组、数字、对象、字符串、函数等操作的一些方法
来感受下啥叫纯与不纯
数组的 slice 和 splice 分别是:纯函数和不纯的函数
slice: 返回数组中的指定部分,不会改变原数组
splice: 对数组进行操作返回该数组,会改变原数组
let numbers = [1, 2, 3, 4, 5]
// 纯函数
numbers.slice(0, 3)// => [1, 2, 3]
numbers.slice(0, 3)// => [1, 2, 3]
numbers.slice(0, 3)// => [1, 2, 3]
// 不纯的函数
numbers.splice(0, 3)// => [1, 2, 3]
numbers.splice(0, 3)// => [4, 5]
numbers.splice(0, 3)// => []
可以看到每次都是相同的输入,slice每次都是相同的输出,所以他是纯函数,由于splice会改变原数组,虽然相同输入,每次输出都变了,所以不纯
函数式编程不会保留计算中间的结果,所以变量是不可变的(无状态的)
我们可以把一个函数的执行结果交给另一个函数去处理
就比如这个slice这个纯函数,我们在调用他的时候会传递参数,然后会获取结果,而函数内部的结果我们是无法获取到的,也就是他不会保留内部的计算中间的结果,所以我们认为函数式编程的变量是不可变的
所以在基于函数式编程的过程中,我么会经常需要一些细粒度的纯函数,要是自己去写细粒度的纯函数,要写非常多,并不方便,所以我们有好多函数式编程的库,比如说Lodash,有了这些细粒度的函数,我们可以把一个函数的执行结果交给另一个函数去处理,我们就能组合出功能更强大函数
Lodash
去官网看看吧,都是些好用的方法,https://www.html.cn/doc/lodash/
纯函数的好处
可缓存
因为纯函数对相同的输入始终有相同的结果,所以可以把纯函数的结果缓存起来
假设有一个超复杂的计算的函数,比如要计算地球的体积,他的入参是地球半径,每计算一次要耗时一秒,那么我们就可以把计算结果储存起来,因为对相同的输入始终有相同的结果,其中lodash里有这么个记忆函数memoize
const _ = require('lodash')
function getVolume (r) {
console.log("半径是" + r)
return 4 / 3 * Math.PI * r * r *r
}
let getVolumeWithMemory = _.memoize(getVolume)
console.log(getVolumeWithMemory(10))
console.log(getVolumeWithMemory(10))
console.log(getVolumeWithMemory(10))
// 输出:
// 半径是10
// 4188.79
// 4188.79
// 4188.79
被memoize包裹后形成的getVolumeWithMemory,入参和getVolume是一样的,可以看到半径是10只执行了一遍,10=>4188.79这个结果就被存起来了,下次再遇到,就直接拿出结果了,不需要再执行了。
我们自己模拟一个memoize
function memoize (f) {
let cache = {}
// 这个cache就是用来缓存结果的,用f的入参当key,用出参当value,比如上面的getVolumeWithMemory执行完后就形成了{10 : 4188.79}
return function () {
let key = JSON.stringify(arguments) // arguments可能是个数组或其他形式,所以转成字符串
cache[key] = cache[key] || f.apply(f, arguments) // cache[key]存在就取cache[key],不存在就执行f,因为arguments是数组所以用apply方法
return cache[key]
}
}
可测试
纯函数让测试更方便
并行处理
在多线程环境下并行操作共享的内存数据很可能会出现意外情况
纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数 (es6 新增的Web Worker,让js有了多线程能力)
副作用
纯函数:对于相同的输入永远会得到相同的输出,而且没有任何可观察的副作用
// 不纯的
let mini = 18
function checkAge (age) {
return age >= mini
}
//你看那个不纯的,你敢保证每次输入20都返回true么,因为它依赖了一个外部变量,无法知晓此变量何时会被篡改
// 纯的(有硬编码,后续可以通过柯里化解决)
function checkAge (age) {
let mini = 18
return age >= mini
}
副作用让一个函数变的不纯(如上例),纯函数的根据相同的输入返回相同的输出,如果函数依赖于外部的状态就无法保证输出相同,就会带来副作用。
副作用的来源除了一些全局变量,还有配置文件,数据库,获取用户的输入等等
所有的外部交互都有可能带来副作用,副作用也使得方法通用性下降不适合扩展和可重用性,同时副作用会给程序中带来安全隐患给程序带来不确定性,比如用户的账号密码是要存在数据库而非函数里的,所以副作用不可能完全禁止,但要尽可能控制它们在可控范围内发生。
柯里化
这里我们先上一个案例,用柯里化来解决上一个例子中硬编码的问题
function checkAge (age) {
let mini = 18
return age >= mini
}
// 既然有硬编码,那我们通过把min字段传进去,不就解决了么,于是
// 普通纯函数
function checkAge (age, min) {
return age >= min
}
checkAge(20, 18)
checkAge(24, 18)
checkAge(26, 30)
// 假设经常以18,30为基准值,每次都输入18,30就过于重复了
// 想想我们之前在闭包那里是怎么处理的
// 柯里化
function checkAge (min) { //既然经常是基准值不变的,所以就让基准值通过闭包储存起来
return function (age) {
return age >= min
}
}
let checkAge18 = checkAge(18)
let checkAge30 = checkAge(30)
checkAge18(24) // 再判断数字的时候就不用输入18了,这就是函数柯里化
checkAge18(20)
// ES6 写法
let checkAge = min => (age => age >= min)
柯里化:
当一个函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变)
然后返回一个新的函数接收剩余的参数,返回结果
我们看看普通纯函数变成柯里化函数的过程是不是就是如上定义一样,还挺套路的
Lodash中的柯里化
既然如此套路,Lodash中也有通用的柯里化方法curry
_.curry(func)
功能:创建一个函数,该函数接收一个或多个 func 的参数,如果 func 所需要的参数都被提供则执行 func 并返回执行的结果。否则继续返回该函数并等待接收剩余的参数。
参数:需要柯里化的函数
返回值:柯里化后的函数
const _ = require('lodash')
// 要柯里化的函数
function getSum (a, b, c) {
return a + b + c
}
// 柯里化后的函数
let curried = _.curry(getSum)
// 测试
curried(1, 2, 3)
curried(1)(2)(3)
curried(1, 2)(3) // 输出结果都是6
柯里化让多元函数转变成了一元函数(几个入参就是几元函数,上面的getSum就是三元函数)
柯里化案例
用上述的curry方法来实现一个案例:提取一个字符串中的所有数字
// 面向过程的方式, 正则match
''.match(/\d+/g) // 数字
// 如果改成提取数组中含有数字的元素,上面的方法就不通用了,所以我们用柯里化来包装一下
const _ = require('lodash')
let match = _.curry(function (reg, str) {
return str.match(reg)
})
// 让他具有特定功能
let findStrNum = match(/\d+/g) //寻找数字
let findStrSpace = match(/\s+/g) //寻找空格
// 试一试
console.log(findStrNum('asd1234')) // true
console.log(findStrSpace('asd1234')) // false
// OK,到这里,关于字符串的match就都可以实现了
// 现在我们要继续用上面方法,来实现数组中的提取含有数字的项
// 数组需要循环,我们来一个柯里化的filter
let filter = function (fn, arr) { // 这里的fn是要做操作的函数
return arr.filter(fn) // 这个filter是数组的filter方法,别搞混
}
let filterCurry = _.curry(filter) // 柯里化
// 让他具有特定功能, 那么就可以传进去上面定义的findStrNum
let findArrNum = filterCurry(findStrNum)
// 试一试
console.log(findArrNum(['abc123', 'abc']))
最后我们就用函数式的方式,实现了这个功能,可能觉得这样写非常麻烦,还不如自己去正则写来实现,但是你要清楚的是,将来这些函数,我们可以不停的重复使用。
const _ = require('lodash')
let match = _.curry(function (reg, str) {
return str.match(reg)
})
let findStrNum = match(/\d+/g)
let findStrSpace = match(/\s+/g)
let filterCurry = _.curry(function (fn, arr) {
return arr.filter(fn)
})
let findArrNum = filterCurry(findStrNum)
let findArrSpace = filterCurry(findStrSpace)
看看上面这些东西,定义一次,就可以在你的工程中无数次的重复使用,能避免自己造轮子中的小bug
模拟 _.curry() 的实现
// 先来看一下这玩意当时是如何使用的
const _ = require('lodash')
function getSum (a, b, c) {
return a + b + c
}
let curried = _.curry(getSum)
curried(1, 2, 3) // 它的调用形式分为传入全部参数,或部分参数
curried(1, 2)(3)
// 自己实现
function curry (func) {
return function curriedFn (...args) {
// 判断实参和形参的个数
if (args.length < func.length) {
return function () {
return curriedFn(...args.concat(Array.from(arguments))) // 这里面就是把每次的参数结合起来,再次调用curriedFn
}
}
// 实参和形参个数相同,调用 func,返回结果
return func(...args)
}
}
柯里化总结
柯里化可以让我们给一个函数传递较少的参数得到一个已经记住了某些固定参数的新函数
这是一种对函数参数的'缓存'
让函数变的更灵活,让函数的粒度更小
可以把多元函数转换成一元函数,可以组合使用函数产生强大的功能
下篇再整理下函数的组合,以避免柯里化后洋葱圈似的代码
函数式编程(三)
发表评论 (审核通过后显示评论):