如何理解函数式中纯函数

函数式编程中的函数,指的就是纯函数,纯函数的概念就是对于一个函数来说,使用相同的输入始终会得到相同的输出,而且没有可观察到的副作用。关于副作用我们后面在解释。这里我们只讨论相同的输入始终会得到相同的输出。

纯函数其实就是数学中函数的概念,他是用来描述输入和输出的映射关系。y=f(x);

我们这里通过数组的两个方法slice和splice演示一下纯函数和不纯的函数。slice是返回数组中的指定部分,不会改变原数组,splice是对数组进行操作,会改变原数组。

我们这里调用了三次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));

我们发现每一次打印的结果都是不同的,因为每一次调用的时候都会修改原数组,每一次都会移除掉数组中的两个元素。这里相同的输入得到的输出是不一样的所以splice这个方法是不纯的函数。

接下啦我们自己来写一个纯函数,比如我们写一个计算两个数的和的函数。

对于纯函数来说,比如要有输入,也要有输出,我们这里多次调用,得到的结果都是相同的。

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。

const _ 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中的console, 第二次调用getAreaWithMemory的时候并没有打印getArea中的console。但是两次调用getAreaWithMemory都返回了相同的结果。

这就说明函数getArea被缓存了,这里我们来模拟一下memoize内部是如何实现纯函数的缓存的。

根据memoize我们知道,这个函数执行的时候要传入一个函数f作为参数,这个f就是真实的函数,也就是上面例子中的getArea,并且返回值也是一个函数。函数的内部要存在一个对象缓存函数f执行的结果,我们可以用f函数传入的参数作为对象的键,因为用户实际调用的是返回的这个参数,所以形参应该在返回的函数中,f的执行结果作为对象的值。

在返回的函数中我们需要存储传入的参数作为键,然后判断cache中是否存在该键对应的值,如果存在,直接返回该值,如果不存在,则调用f函数,并且将执行结果存入cache再返回执行结果。

这里我们通过apply来调用函数f,因为我们并不知道有多少个参数,所以我们使用arguments参数集合,apply第二个参数可以接收一个参数集合。第一个参数是函数调用的this,这里不是主要的,我们可以写成f它自身。

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, 可以开启多线程,但是大多数我们使用js还是单线程的。

副作用

纯函数的另一个特性是没有任何可观察的副作用,我们通过一段代码来演示什么是副作用

let mini = 18;
function checkAge (age) {
    return age >= mini;
}

checkAge(20); // true
mini = 28;
checkAge(20); // false

上面这个函数就是不纯的,因为我们知道,对于一个纯函数来说,相同的输入永远得到想用的输出,而checkAge这个函数,依赖了外部变量mini,这个变量是可能发生变化的。所以并不能保证相同的输入始终返回相同的输出,所以他是不纯的,也就是存在副作用。

副作用让一个函数变得不纯,纯函数的根据是相同的输入返回相同的输出,如果函数依赖于外部的状态就无法保证输出相同,就会带来副作用,也就是我们这个mini变量带来了副作用,让函数变得不纯。

除了全局变量,副作用的来源还有配置文件,我们有可能会从配置文件中获取信息。还有数据库和获取用户输入等等,这些都会带来副作用。

总结就是所有的外部交互都会产生副作用,副作用也会使得方法通用性下降不适合以后的扩展和重用。同时副作用也会给程序中带来一些安全隐患,比如说用户的输入可以带来攻击。

虽然副作用存在这么多问题,但是副作用是不可能完全禁止的,因为我们不可能将用户名密码等一些信息记录到代码中,这些信息还是需要放在数据库中的,我们应该尽可能的控制副作用在可控的范围内发生。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值