深入理解函数式编程(二)

459 阅读4分钟

「这是我参与2022首次更文挑战的第5天,活动详情查看:2022首次更文挑战

TIP 👉 五更钟漏欲相催,四气推迁往复回。帐里残灯才去焰,炉中香气尽成灰。渐看春逼芙蓉枕,顿觉寒销竹叶杯。守岁家家应未卧,相思那得梦魂来。——唐·孟浩然《除夜有怀》

前言

我们在上一篇讲了为什么要学函数式编程以及什么是函数式编程和高阶函数,接下来我们继续介绍函数式编程相关的内容

纯函数

纯函数概念

  • 纯函数:相同的输入永远会得到相同的输出**,而且没有任何可观察的副作用
  1. 纯函数就类似数学中的函数(用来描述输入和输出之间的关系),y = f(x)

image.png

  • lodash 是一个纯函数的功能库,提供了对数组、数字、对象、字符串、函数等操作的一些方法

  • 数组的slicesplice分别是:纯函数和不纯的函数

    1)slice返回数组中的指定部分,不会改变原数组

    2)splice对数组进行操作返回该数组,会改变原数组

let numbers = [12345]
// 纯函数
numbers.slice(03)
// => [1, 2, 3]
numbers.slice(03)
// => [1, 2, 3]
numbers.slice(03)
// => [1, 2, 3]

// 不纯的函数
numbers.splice(03)
// => [1, 2, 3]
numbers.splice(03)
// => [4, 5]
numbers.splice(03)
// => []
  • 函数式编程不会保留计算中间的结果,所以变量是不可变的(无状态的)

  • 我们可以把一个函数的执行结果交给另一个函数去处理

纯函数的好处

  • 可缓存 因为纯函数对相同的输入始终有相同的结果,所以可以把纯函数的结果缓存起来

  • 自己模拟一个 memoize 函数

function memoize (f) {
    let cache = {}
    return function () {
        let arg_str = JSON.stringify(arguments)
        cache[arg_str] = cache[arg_str] || f.apply(f, arguments)
        return cache[arg_str] 
    }
}
  • 可测试 纯函数让测试更方便

  • 并行处理

在多线程环境下并行操作共享的内存数据很可能会出现意外情况

纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数 (Web Worker)

副作用

纯函数:对于相同的输入永远会得到相同的输出,而且没有任何可观察的副作用

// 不纯的
let mini = 18
function checkAge (age) { 
    return age >= mini
}

//  纯的(有硬编码,后续可以通过柯里化解决)
function checkAge (age) { 
    let mini = 18
    return age >= mini
}

副作用让一个函数变的不纯(如上例),纯函数的根据相同的输入返回相同的输出,如果函数依赖于外部的状态就无法保证输出相同,就会带来副作用。

副作用来源:

  • 配置文件
  • 数据库
  • 获取用户的输入
  • ......

所有的外部交互都有可能带来副作用,副作用也使得方法通用性下降不适合扩展和可重用性,同时副作用会给程序中带来安全隐患给程序带来不确定性,但是副作用不可能完全禁止,尽可能控制它们在可控范围内发生。

柯里化

  • 使用柯里化解决上一个案例中硬编码的问题
function checkAge (age) { 
    let min = 18
    return age >= min
}

// 普通纯函数

function checkAge (min, age) { 
    return age >= min
}

checkAge(1824)
checkAge(1820)
checkAge(2030)
// 柯里化
function checkAge (min) { 
    return function (age) {
        return age >= min
    }
}

// ES6 写法
let checkAge = min => (age => age >= min)
let checkAge18 = checkAge(18) let checkAge20 = checkAge(20)
checkAge18(24) checkAge18(20)

lodash中的柯里化函数

  • _.curry(func)

功能:创建一个函数,该函数接收一个或多个 func 的参数,如果 func 所需要的参数都被提供则执行 func 并返回执行的结果。否则继续返回该函数并等待接收剩余的参数。

参数:需要柯里化的函数

返回值:柯里化后的函数

const _ = require('lodash')

// 要柯里化的函数

function getSum (a, b, c) {
    return a + b + c 
}

// 柯里化后的函数
let curried = _.curry(getSum) 
// 测试
curried(123)
curried(1)(2)(3)
curried(12)(3)

模拟_.curry()的实现

function curry (func) {
    return function curriedFn (...args) {
        // 判断实参和形参的个数
        if (args.length < func.length) {
            return function () {
                return curriedFn(...args.concat(Array.from(arguments)))
            }
        }
        // 实参和形参个数相同,调用 func,返回结果
        return func(...args)
    }
}

总结

柯里化可以让我们给一个函数传递较少的参数得到一个已经记住了某些固定参数的新函数这是一种对函数参数的'缓存'

让函数变的更灵活,让函数的粒度更小

可以把多元函数转换成一元函数,可以组合使用函数产生强大的功能

函数组合

纯函数和柯里化很容易写出洋葱代码h(g(f(x)))

获取数组的最后一个元素再转换成大写字母, .toUpper(.first(_.reverse(array)))

image.png

函数组合可以让我们把细粒度的函数重新组合生成一个新的函数

管道

下面这张图表示程序中使用函数处理数据的过程,给 fn 函数输入参数 a,返回结果 b。可以想想 a 数据通过一个管道得到了 b 数据。

image.png

当 fn 函数比较复杂的时候,我们可以把函数 fn 拆分成多个小函数,此时多了中间运算过程产生的 m 和 n。

下面这张图中可以想象成把 fn 这个管道拆分成了3个管道 f1, f2, f3,数据 a 通过管道 f3 得到结果 m,m

再通过管道 f2 得到结果 n,n 通过管道 f1 得到最终结果 b

image.png

fn = compose(f1, f2, f3)

b = fn(a)

函数组合

函数组合 (compose):如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数

函数就像是数据的管道,函数组合就是把这些管道连接起来,让数据穿过多个管道形成最终结果

函数组合默认是从右到左执行

// 组合函数

function compose (f, g) { 
    return function (x) {
        return f(g(x))
    }
}

function first (arr) { 
    return arr[0]
}

function reverse (arr) { 
    return arr.reverse()
}

// 从右到左运行

let last = compose(first, reverse) 
console.log(last([1, 2, 3, 4]))

lodash 中的组合函数

lodash 中组合函数 flow() 或者 flowRight(),他们都可以组合多个函数

flow() 是从左到右运行

flowRight()是从右到左运行,使用的更多一些flow() 是从左到右运行

flowRight是从右到左运行,使用的更多一些

cosnt _ = require('lodash')

const toUpper = s => s.toUpperCase() 
const reverse = arr => arr.reverse() 
const first = arr => arr[0]

const f = _.flowRight(toUpper, first, reverse) 
console.log(f(['one', 'two', 'three']))

模拟实现 lodash 的 flowRight 方法

// 多函数组合

function compose (...fns) { 
    return function (value) {
        return fns.reverse().reduce(function (acc, fn) { 
            return fn(acc)

        }, value)
    }
}
// ES6

const compose = (...fns) => value => fns.reverse().reduce((acc, fn) => fn(acc), value)

函数的组合要满足结合律(associativity):

我们既可以把 g 和 h 组合,还可以把 f 和 g 组合,结果都是一样的

// 结合律(associativity)

let f = compose(f, g, h)

let associative = compose(compose(f, g), h) == compose(f, compose(g, h))

// true

附录

函数式编程指北

函数式编程入门

「欢迎在评论区讨论」

希望看完的朋友可以给个赞,鼓励一下