函数式编程十分钟掌握纯函数和柯里化
Posted 前端大联盟
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了函数式编程十分钟掌握纯函数和柯里化相关的知识,希望对你有一定的参考价值。
茫茫人海中与你相遇
相信未来的你不会很差
链接:https://juejin.cn/post/6893110732423397383
纯函数
函数式编程中的函数,指的都是纯函数,纯函数的概念简单来说就是对于一个函数来说,使用相同的输入始终会得到相同的输出,而且没有可观察到的副作用。关于副作用我们后面再解释。这里我们只讨论相同的输入始终会得到相同的输出。
slice
splice
slice
首先我们在这里调用三次slice,注意纯函数的定义,相同的输出始终会得到相同的输出。
let array = [1, 2, 3, 4, 5, 6];
console.log(array.slice(0, 2));
console.log(array.slice(0, 2));
console.log(array.slice(0, 2));
测试发现三次打印的结果都是一样的,所以slice就是一个纯函数。接下来我们再来演示一下
splice
let array = [1, 2, 3, 4, 5, 6];
console.log(array.splice(0, 2));
console.log(array.splice(0, 2));
console.log(array.splice(0, 2));
function getSum (n1, n2) {
return n1 + n2;
}
console.log(getSum(1, 2));
console.log(getSum(1, 2));
console.log(getSum(1, 2));
纯函数的优点
纯函数的第一个好处是可缓存,因为纯函数对相同的输入始终会有相同的输出,所以可以把纯函数的结果进行缓存。
为什么要缓存函数呢,比如说我们有个函数,执行起来特别耗时,但是这个函数需要多次调用,那每次调用这个函数的时候都需要去等一段时间,才能获取到这个结果,所以他对性能来说是有影响的,使用缓存可以很好的解决这个问题,提高程序的性能。
lodash
memoize
getArea
memoize
为了演示这个函数被缓存,我们可以在
getArea
getAreaWithMemory
c1onst _ from 'lodash';
function getArea (r) {
console.log(`getArea 执行了`);
return Math.PI * r * r;
}
const getAreaWithMemory = _.memoize(getArea);
console.log(getAreaWithMemory(3)));
console.log(getAreaWithMemory(3)));
可以发现,当我们第一次调用
getAreaWithMemory
getArea
getAreaWithMemory
getArea
getAreaWithMemory
这就说明函数
getArea
memoize
根据
memoize
getArea
在返回的函数中我们需要存储传入的参数作为键,然后判断
cache
cache
这里我们通过
apply
function memoize (f) {
let cache = {};
return function () {
let key = JSON.stringify(arguments);
cache[key] = cache[key] || f.apply(f, arguments);
return cache[key];
}
}
这里其实还有一点问题的,假设缓存的值是
false,0,null, undefined
到这里关于纯函数的第一个好处,可缓存,我们这里就演示完了,将来我们在写程序的时候就可以通过这种方式来提高程序的性能。
纯函数的第二个好处就是可测试,因为纯函数始终有输入和输出,而单元测试就是在断言函数的结果,所以我们所有的纯函数都是可测试的函数。
另外纯函数还方便并行处理,因为在多线程环境下并行操作共享的内存数据很可能会出现意外情况,假设多个线程同时修改一个全局变量,并且每个线程修改后的值都不同,那这个变量的值最终是没办法确定的。纯函数就不会有这样的问题,因为他只依赖参数,他不能访问共享的内存数据,也就是自己作用域外的数据,所以在并行环境下可以任意运行纯函数。
在以前这和js基本上是没关系的,因为js是单线程的,但是在ES6之后,js新增了
Web Worker
副作用
纯函数的另一个特性是没有任何可观察的副作用,我们通过一段代码来演示什么是副作用
let mini = 18;
function checkAge (age) {
return age >= mini;
}
checkAge(20); // true
mini = 28;
checkAge(20); // false
上面这个函数就是不纯的,因为我们知道,对于一个纯函数来说,相同的输入永远得到想用的输出,而
checkAge
mini
副作用让一个函数变得不纯,纯函数的根据是相同的输入返回相同的输出,如果函数依赖于外部的状态就无法保证输出相同,就会带来副作用,也就是我们这个
mini
除了全局变量,副作用的来源还有配置文件,我们有可能会从配置文件中获取信息。还有数据库和获取用户输入等等,这些都会带来副作用。
总结就是所有的外部交互都会产生副作用,副作用也会使得方法通用性下降不适合以后的扩展和重用。同时副作用也会给程序中带来一些安全隐患,比如说用户的输入可以带来攻击。
虽然副作用存在这么多问题,但是副作用是不可能完全禁止的,因为我们不可能将用户名密码等一些信息记录到代码中,这些信息还是需要放在数据库中的,我们应该尽可能的控制副作用在可控的范围内发生。
柯里化
这里我们来谈论下函数式编程中另一个重要的概念,柯里化
首先,我们先通过下面的方式将上节代码中不纯的函数变成纯函数。就是将
mini
function checkAge (age) {
let mini = 18;
return age >= mini;
}
但是当我们把这个
mini
function checkAge (min, age) {
return age >= min;
}
checkAge(18, 20);
checkAge(18, 21);
checkAge(18, 22);
这里我们就改造完了,我们根据输入始终会得到相同的输出,因为他不在依赖于外部的变量,并且它里面也没有硬编码。
可以发现,当我们经常使用18这个基准值的时候,这个18就会经常重复,我们想要避免这个1
返回的函数中接收一个
age
age
min
checkAge
checkAge18,checkAge
function checkAge (min) {
return function (age) {
return age >= min;
}
}
let checkAge18 = checkAge(18);
checkAge18(20);
checkAge18(21);
checkAge18(22);
这里可以发现我们在调用的时候不会让基准值重复,因为我们在第一个函数中已经确定下来了。
以上函数调用的方式就是柯里化,那我们这里简单说明一下什么是柯里化。
当我们的函数有多个参数的时候我们可以对这个函数进行改造,我们可以调用一个函数,只传递部分参数,并且让这个函数返回一个新的函数,这个新的函数去接收剩余的参数,并且返回相应的结果,这就是函数的柯里化。
上面的代码不够通用,我们这里介绍一下
lodash
lodash
curry
curry
我们这里演示一下
lodash
curry
我们这里定义一个求三个数和的函数, 柯里化可以将一个多元(参数个数)函数转换为一元函数。我们使用
curried
getSum
curried
const _ = require('lodash');
function getSum (a, b, c) {
return a + b + c;
}
const curried = _curry(getSum);
// curried(1, 2, 3);
// curried(1)(2, 3);
curried(1, 2)(3)
所以我们这里发现,我们通过柯里化过后的函数使用起来非常方便,他可以传递一个参数,可以传入多个参数。
下面我们来模拟一下
lodash
curry
返回的柯里化函数在执行的时候,可以传递全部参数,也可以传递部分参数,当传递全部参数的时候,这个函数就要立即执行,当传递是部分参数的时候,会返回一个新的函数,然后等待接收剩余的参数。
这里我们知道,传递的参数是不固定的,所以我们在函数的内部就要判断一下传入的参数和形参的个数是否相同。我们可以通过ES6的
reset
.args
然后我们需要把形参个数和实参个数进行对比,判断是否相同。实参就是
args
func.length。
function curry (func) {
return function curriedFn(...args) {
if (args.length >= func.length) {
return func(...args);
} else {
return function () {
}
}
}
}
当传入部分参数的时候,我们需要将当前传入的参数和之前传入的参数合并到一起,然后与原函数的参数进行对比。
新传入的参数我们用
...newArgs
args
newArgs
curriedFn
curriedFn
function curry (func) {
return function curriedFn(...args) {
if (args.length >= func.length) {
return func(...args);
} else {
return function (...newArgs) {
return curriedFn(...args.concat(newArgs));
}
}
}
}
这里我们就写完了,最后我们来总结一下函数的柯里化。
函数的柯里化可以让我们给一个函数传递较少的参数,得到一个已经记住了某些固定参数的新函数。也就是柯里化可以实现函数的参数分步传递,如果传递的参数不满足函数的参数要求,就会返回一个新的函数,可以在新的函数中继续传递后面的参数。前面传递的参数已经被记录在新函数里面了。
柯里化的内部使用了闭包对函数的参数进行了缓存,柯里化可以让函数变得更灵活,因为可以生成一些粒度更小的函数。我们这么做的目的是为了后续学习组合的时候使用。
使用柯里化可以把多元的函数转化成一元的函数,可以把这些一元函数组合成功能更强大的函数。
我们在虚拟的空间与你相遇,期待可以碰撞出不一样的火花
以上是关于函数式编程十分钟掌握纯函数和柯里化的主要内容,如果未能解决你的问题,请参考以下文章