函数式编程--纯函数(Pure Function)

288 阅读5分钟

基本概念

在程序设计中,若一个函数符合以下要求,则它可能被认为是纯函数(Pure Function):

  • 相同的输入值时,需产生相同的输出
  • 函数的输出和输入值以外的其他隐藏信息或状态无关,也和由I/O设备产生的外部输出无关
  • 不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等

相同的输入值时,需产生相同的输出

纯函数是这样一种函数,即相同的输入,永远会得到相同的输出,否则就不是纯函数。

例如,下面例子,无论上下文是什么,无论调用多少次double(2)返回值永远是4,可以直接用于替换double(2);而random(2)每次调用返回一个0~n之间的随机数,返回值每次都可能发生改变。所以,double是纯函数,而random不是。

function double(n){
    return n * 2
}

double(2) // 4
double(2) // 4

function random(n){
    return n * Math.random()
}

random(2) // 0.3131058434716416
random(2) // 0.46288851095052275

函数的输出和输入值以外的其他隐藏信息或状态无关

纯函数的输出可以不用和所有的输入值有关,甚至可以和所有的输入值都无关。但纯函数的输出不能和输入值以外的任何元素有关。比如,函数依赖一个全局变量,或者作用域以外的变量,或者一个dom元素的属性或者内容,这些信息随时可能会发生改变,从而影响到函数的输出。

下面double函数中的i是外部变量,随时可能被修改,进而影响到double函数的输出,所以这里double不是纯函数。getInfo()输出取决dom的内容信息,也不是纯函数。

var i = 2

function double(n){
    return n * 2
}

function getInfo(){
    return `get info from element:${$('#info').text()}`
}

我们可以保证外部元素信息是不可变的,从而保证函数的输入和输出的一致性。只要保证函数不受外部因素影响、相同的输入值,其输出值也相同,且对外不产生副作用,就是纯函数。但是,javascript中保证变量不可变,是很困难的。

  • 不可变性的讨论

const关键字声明的number、boolean、string类型的常量是不可变的。这里的double是纯函数

const i = 2

function double(n){
    return n * i
}

double(2) // 4

i = 3 // TypeError: Assignment to constant variable
double(2) // 4

我们无法保证const声明的object类型的数据不可变。这里的double不是纯函数

const o = { i: 2 }

function double(n){
    return n * o.i
}

double(2) // 4

o.i = 3

double(2) // 6

Object.freeze可以冻结对象,保证其不可变。下面的double是纯函数

var o = Object.freeze({i:2})

function double(n){
    return n * o.i
}

double(2) // 4

o.i = 3

double(2) // 4

Object.freeze无法保证对象深层次的属性不可变

var o = Object.freeze({k:{i:2}})

function double(n){
    return n * o.k.i
}

double(2) // 4

o.k.i = 3

double(2) // 6

deepFreeze可以保证object的不可变


const deepFreeze = (obj) => {
  for (let prop of Object.keys(obj)) {
    if (typeof obj[prop] === 'object') {
      Object.freeze(obj[prop]);
      deepFreeze(obj[prop]);
    }
  }
}

不能有语义上可观察的函数副作用

纯函数不应该对函数外部元素产生副作用。如果执行过程中修改和影响了外部状态,那么就不是纯函数。比如,触发事件、使用输入输出设备、修改外部变量、修改dom属性或者内容、发送一个信息到服务端、保存和修改文件信息等,诸如此类的副作用还有很多。

下面实例中,log函数向控制台打印信息,也被认为是副作用。


function log(info){
    console.log(info)
}

下面实例,change函数虽然修改的是输入的参数值,但是同时改变了外部变量obj,也是副作用。

var obj = {
    name: 'zhangsan'
}

function change(o){
    o.name = 'lisi'
}

change(o)

数组的slice和splice方法,具备相似的功能,性质区别却很大。slice没有副作用,但是splice却是有副作用的。


let arr = ['a','b','c']

let arr1 = arr.slice(1,2) 
// arr1 ===  ['b']
// arr === ['a','b','c']
let arr2 = arr.splice(1,1)
// arr2 === ['b','c']
// arr === ['a']

纯函数的优点和用途

没有副作用,利于重构

纯函数没有副作用,不会影响函数外部元素的状态。可以放心的对纯函数进行重构,而不必担心会对其他模块产生任何影响。

不受外部状态影响,利于并行处理

纯函数完全独立于外部状态,因此,它们不受所有与共享可变状态有关问题的影响。它们的独立特性也使其成为跨多个 CPU 以及整个分布式计算集群进行并行处理的极佳选择,这使其对许多类型的科学和资源密集型计算任务至关重要。

相同的输入,输出永远固定,利于测试,利于缓存

纯函数确保输入和输出的一致性,对于测试至关重要。我们不需要伪造一个“真实的”环境,或者每一次测试之前都要配置、之后都要断言状态(assert the state)。只需简单地给函数一个输入,然后断言输出就好了。


let greeting = 'Hello'

// 依赖外部变量greeting,非纯函数
function greet1 (name) {
  return greeting + ' ' + name
}

// 纯函数
function greet2 (greeting, name) {
  return greeting + ' ' + name
}

// 无法保证测试的正确性
describe('greet1', function() {
    it('shows a greeting', function() {
        expect(greet1('Savo')).toEqual('Hello Savo')
    })
})

// 可以保证测试的正确性
describe('greet2', function() {
    it('shows a greeting', function() {
        expect(greet2('Hello','Savo')).toEqual('Hello Savo')
    })
})

基于纯函数的输入与输出的一致性,可以上一次执行的结果缓存,以供后面使用

/**
 * fn是纯函数
 */
export function cached(fn) {
    const cache = Object.create(null)
    return function cachedFn(str) {
        const hit = cache[str]
        return hit || (cache[str] = fn(str))
    }
}

/**
 * 实例
 */
export const capitalize = cached((str) => {
    return str.charAt(0).toUpperCase() + str.slice(1)
})

// 第一次调用,缓存cache['hello world'] = fn('hello world')
capitalize('hello world') // Hello world

// 第二次调用直接从cache['hello world']中取出,不会经历fn执行
capitalize('hello world') // Hello world

参考资料

zh.wikipedia.org/wiki/%E7%BA… llh911001.gitbooks.io/mostly-adeq… juejin.cn/post/684490… zhuanlan.zhihu.com/p/121627485 levelup.gitconnected.com/functional-…