函数式编程主食

2,650

函数式编程是什么?

在函数式编程中,函数就是一个管道(pipe)。这头进去一个值,那头就会出来一个新的值,没有其他作用。——阮一峰 函数式编程入门教程

函数式编程本质上是一种数学运算。因为是数学运算所以自然就会涉及到加减乘除等运算和交换律结合律同一律分配律等运算法则。如果要函数顺利的进行数学运算,就要求函数必须是纯的,不能有副作用,即纯函数。但如果只是简单的将纯函数用于复杂的加减乘除运算,则会写出一堆看起来杂乱无章的、不符合人类阅读习惯和编码直觉的代码。因此函数式编程需要借助组合函数、柯里化、递归、闭包和各种各样的高阶函数让代码看起来更符合人类的直觉和逻辑思维的方式。

再说一等公民

当我们在说函数是“一等公民”的时候,不要想当然的以为函数就是 js 世界里的老大了,我们实际上说的是它们和其他对象都一样:就是普通公民。

作为一等公民,函数可以被赋值给另外一个变量,然而编程中却有很多这样的神奇操作:

const hi = name => `Hi, ${name}`
const greeting = name => hi(name)

实际上调用 hi('girl')greeting('gril') 的结果无论如何都是完全一样的,greeting 函数所作的不过是调用并返回 hi 函数而已。但实际上完全没必要如此脱裤子放屁多此一举,直接把 hi 函数赋值给 greeting 变量即可:

const hi = name => `Hi, ${name}`
const greeting = hi

懂了吗?那再看下下面这个:

// 太傻了
const getServerStuff = callback => ajaxCall(json => callback(json)) // ajaxCall 是外部封装好的接口函数

这么长的函数,看都看不懂,我们来简化下:

// 这行
ajaxCall(json => callback(json));

// 等价于这行
ajaxCall(callback);

// 那么,重构下 getServerStuff
const getServerStuff = callback => ajaxCall(callback);

// ...就等于
const getServerStuff = ajaxCall // <-- 看,没有括号哦

拆到底就是个赋值操作而已~ 一定要避免这种气人又尴尬的函数啊~

纯函数

纯函数是这样一种函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用。

从这句话中可以看出要成为纯函数有两个充分必要条件:

  1. 相同的输入会得到相同的输出(也叫引用透明性)。比如若是函数的输入参数是数字返回值是数组,就不会发生输入参数是数字返回值是数字的情况。
  2. 没有任何可观察的副作用。也就是说函数在执行的过程中不依赖于外部的状态 / 也不会改变外部的状态。

比如对于 slicesplice 函数,输入数字参数,总能返回数组。但是不同之处在于 splice 在执行过程中会改变原数组,而 slice 在没有这样的副作用。所以 slice 是纯函数而 splice 不是,如下所示:

let nums = [1, 2, 3, 4, 5]
let a = nums.slice(0,2) // [1, 2]
console.log(nums) // [1, 2, 3, 4, 5]

let nums = [1, 2, 3, 4, 5]
let b = nums.splice(0, 2) // [1, 2]
console.log(nums) // [3, 4, 5]

戏剧性的是:纯函数就是数学上的函数,而且是函数式编程的全部。

如何才能编写无副作用的纯函数是学习函数式编程的关键。

因为不依赖外部的状态,所以纯函数编程需要借助一些工具函数,来使函数的传参看起来优雅且容易理解。

声明式编程

函数式编程属于声明式编程范式,函数式编程的目的是使用函数来抽象作用在数据之上的控制流和操作,从而在系统中消除副作用并减少对状态的改变。

// 命令式方式
var array = [0, 1, 2, 3]
for(let i = 0; i < array.length; i++) {
    array[i] = Math.pow(array[i], 2)
}
array // [0, 1, 4, 9]

// 声明式方式
[0, 1, 2, 3].map(num => Math.pow(num, 2)) // [0, 1, 4, 9]

命令式编程注重代码执行过程,上面代码使用了命令控制结构 for 循环,正是这种命令控制语句导致了代码的死板和难以复用。函数式编程不关注代码具体如何执行和数据如何穿过函数,而关注代码执行结果。这也决定了函数式编程需要倚重一些工具函数( 如 map filter ruduce find 等数组函数和 curry compose 等工具函数 )。

柯里化

俗话说一口吃不成胖子,记得第一次接触柯里化是在红宝书里面,当时看了好几遍还是一脸懵逼,不知所云。所以柯里化函数是需要一定能力的抽象思维的。

柯里化是一种将使用多个参数的函数转换成一系列使用一个或多个参数的函数的技术。你可以一次性地传递所有参数调用函数,也可以每次只传一个参数分多次调用。

function sub_curry(fn, ...args) { // sub_curry 用来缓存传入的参数
  return function() {
    return fn.apply(this, args.concat([...arguments])
  }
}
function curry(fn, length) {
  length = length || fn.length
  return function(...args) {
    if (args.length < length) {
      var combined = [fn].concat(args)
      return curry(sub_curry.apply(this, combined), length - args.length) // 递归调用自身返回一个固定部分参数的函数
    } else {
      return fn.apply(this, args)
    }
  }
}
var fn = curry(function(a, b, c) {
  return [a, b, c]
})
fn("a", "b", "c") // ["a", "b", "c"]
fn("a", "b")("c") // ["a", "b", "c"]
fn("a")("b")("c") // ["a", "b", "c"]
fn("a")("b", "c") // ["a", "b", "c"]

curry 函数的原理是利用闭包将原函数的一部分参数存起来,如果参数小于原函数形参的数量就返回一个新函数以便继续传参调用,如果参数等于原函数的参数了就执行原函数。

import { curry } from 'lodash'

function add(a, b) {
  return a + b
}
var addCurry = curry(add) // 柯里化add

var increment = addCurry(1) // 生成新函数 increment 执行会将入参增加 1
increment(10) // 11

var addTen = addCurry(10)) // 生成新函数 increment 执行会将入参增加 1
addTen(1) // 生成新函数 addTen 执行会将入参增加 10

可以看到利用柯里化技术,能够生成固定了部分参数的新函数。

组合

我们认为组合是高于其他所有原则的设计原则,这是因为组合让我们的代码简单而富有可读性。

组合顾名思义就是将不同的函数组合起来生成一个新的函数。组合的参数必须都是纯函数。

function compose (...fns) {
  return function (...args) {
    return fns.reduceRight((arg , fn, index) => {
      if (index === fns.length - 1) {
        return fn(...arg)
      }
      return fn(arg)
    }, args)
  }
}

function toUpperCase(str) {
    return str.toUpperCase()
}
function split(str){
  return str.split('');
}
function reverse(arr){
  return arr.reverse();
}
function join(arr){
  return arr.join('');
}

const turnStr = compose(join, reverse, split, toUpperCase)
turnStr('emosewa si nijeuj') // JUEJIN IS AWESOME

组合函数好像一个管道一样,将参数从右向左依次执行并将结果传递给左边的参数。

组合函数还符合结合律,组合的调用分组不重要,所有结果都是一样的:

const turnStr1 = compose(compose(join, reverse), split, toUpperCase)
turnStr1('emosewa si nijeuj') // JUEJIN IS AWESOME

const turnStr2 = compose(compose(join, reverse), split, toUpperCase)
turnStr2('emosewa si nijeuj') // JUEJIN IS AWESOME

结合律的一大好处是任何一个函数分组都可以被拆开来,然后再以它们自己的组合方式打包在一起。当然这个就看个人的抽象能力和编码能力了。

总结

函数式编程不是一朝一夕就能学会的,而是要在实际开发中逐渐学习和熟练的。在实际编程中,尽量的利用 curry compose 等工具函数和递归将代码控制逻辑抽先化,逐渐舍弃命令式开发而习惯编写声明式的代码。