初探函数式编程

前言

函数式编程,起源于数学分支的范畴论,范畴论认为,所有概念体系都可以抽象出一个个范畴,任何事物只要找出他们之间的联系,就能定义。
同一个范畴的成员,就是不用状态的“transformation”,通过“态射”,一个成员可以变形为另一个成员。

函数式编程技术理论

    1. 函数式编程并不是用函数来编程,主旨在于将复杂的函数合成简单的函数
    1. 函数式编程的火热是随着 React 的 HOC 而升温
    1. 函数式编程贴近数学函数,本质上是一种运算
    1. 固定的输入,固定的输出

函数式编程基本理念

    1. 函数式一等公民,意 ☞ 函数与其他数据类型一样,处于平等地位,即可以作为参数传递,也可以作为其他函数的返回值
    1. 只用“表达式”,不用“语句”,可以理解为不存在逻辑判断
    1. 没有“副作用”,即不会影响其他外部变量,也不会依赖于外部的状态
    1. 引用透明,函数执行只靠参数
    1. 函数式编程常用的方法有 map 和 reduce

专业术语

纯函数

对于相同的输入,会得到相同的输出,而且没有任何副作用,不依赖于外部状态

优缺点

  • 纯函数可以有效的降低复杂度
  • 纯函数具有缓存性,提高运行效率,比如我们缓存函数,然后判断参数,输出结果
  • 扩展性比较差

let array=[1,2,3,4,5]
arr.slice(0,3) // Array.slice是纯函数,对于固定的输入,输出也是固定的

let preAge=18
let  checkAge = age => age + preAge // 函数不纯,引用了外部状态
// 改造纯函数
let checkAge = (age,preAge) => age + preAge

纯函数的纯度

纯函数与数学概念“幂等性”相关,幂等性是指无数次执行后还具有相同的结果,同一参数运行一次与多次函数结果应该一致。

Math.abs(Math.abs(-18))

偏应用函数

传递给函数一部分参数来调用它,返回一个函数去处理剩下的参数,之所以成为“偏”函数,就在于只能处理部分 case 语句的输入,而不能处理所有输入的可能

经典案例

  • 函数柯里化,可以达到预先执行部分函数,减小执行开销,也可以理解为函数的“预加载”
  • 简化执行逻辑

const add=(a,b,c)=>a+b+c
const addPlue=add.bind(null,2,3) // (c)=>2+3+c

这里再介绍下反柯里化函数

反柯里化函数,与柯里化正好相反,目的是扩大适用范围,使原本对象有的方法,扩展到其他对象上的一种手段

// 一个简单的例子,比如我们知道call和apply对于类数组处理比较好,那么我们就可以“借”用过来
function testCoding () {
  const array=Array.prototype.shift.call(arguments)
  return array
}

// 第二个简单的例子,比如我们在是适用understore中,会发现很多方法都是两种用法,例如 get(obj,'name')   obj.get('name'),其实前者就是反柯里化的实现,目的就是为了让函数可以通用(扩展)
Array.prototype.push.uncurry=function(){
  return this.call.bind(this);
}
var push=Array.prototype.push.uncurry();
var arr=[];
push(arr,1);

函数组合

在上面介绍的纯函数和柯里化后,我们最终要去执行我们的函数,尤其是柯里化后的洋葱代码(a(b(c(d)))),为了解决函数嵌套的问题,我们要用到函数组合

经典案例

  • Redux 的 applyMiddleWare
  • Koa 中间件,洋葱模型
  • understore、lodash 等

const compose=(...fnArray)=> ...args => fnArray.reduce((add,func)=>add(func(...args)))
const first=arr=>arr[0]
const second=arr=>arr.reverse()
const last=compose(first,second)
last([1,2,3,4,5])

函数组合,不只是可以直接组合所有函数,也可以拆开组合,相对于业务来说,也是一种扩展
img

compose(f,compose(g,h))
compose(compose(f,g),h)
compose(f,g,h)

抛出概念:组合子

  • 组合子,目的就是为了改变函数执行流向,类似于管道(pipe),比如 compose,是从右往左执行
  • 常见的组合子,例如 compose(组合)、map(映射)、reduce(规约)、curry(柯里化)、reverse(颠倒)、alt(交替)等等。。。。。。

** 这块还有很多没掌握,GG

Point Free

目的就是将对象自带的方法转换成纯函数,在执行函数时,去掉不必要的命名

这段代码,str 作为中间变量,除了让代码变得长了点,没有任何意义

const fn = str => str.toUpperCase().split(',')

为了解决上面的问题,我们改造一下

const toUpperCase = word => word.toUpperCase()
const split = x => (str => str.split(x))
const fn = compose(split(','),toUpperCase)
fn('duan,xl')

咋一看,代码还变多了,但是代码要比上面的简洁通用,我们把上面的步骤拆了出来,可以让其他函数继续组合~~~

声明式与命令式

  • 命令式:通过编写一行又一行的指令去执行一些操作
  • 声明式:通过表达式的方式去表明我们想干什么,函数式编程推荐使用声明式的代码

命令式代码相比与声明式代码,写法不优雅,而且我们要注意执行操作的步骤,而声明式,就会优雅很多

// 命令式
let arr=[];
for(let i=0;i<xxx.length;i++>){
  arr.push(xxx[i].name)
}
// 声明式
const arr=xxx.map(item=>item.name)

函数即数据

其实就是我们常见的 mixin,通过混入的方式,实现函数建模,也就是链式调用,增加业务扩展能力

// 简单搬一下别人的砖
_.mixin({
  chain:_.chain,
  reverse:_.reverse,
  sort:_.sort,
  get:_.get
})
_.chain(object).reverse().sort().get()

总结

  • 函数式编程的使用声明式的代码
  • 要尽量的变成纯函数,对于无副作用的纯函数,我们不需要考虑函数如何实现,只需要关注于业务
  • 在优化代码时,我们只需要专注于函数内部的稳定性
  • 对于纯函数,我们不会依赖外部环境,不需要考虑副作用,可以减轻开发者的心里负担,比如你写了一个函数,不小心把外部变量改掉了,leader 可能会让你祭天~~~~

经典应用

高阶函数

函数当参数,把传入的函数封装,返回这个封装的函数,让这个函数达到更高的抽象度

const add=(a,b)=> a+b

const math=(fn,array)=>fn(array[0],array[1])

math(add,[1,2])

React 中的 HOC 其实也是高阶函数,通过将传入的 component 进行封装,可以让被封装的函数复用,封装后的 component 具有更加灵活的应用

尾递归优化

相比如传统的递归,尾递归从开始就调用自身,不会产生任何的变量依赖,如果我们把普通递归改成尾递归,那么在调用记录中,只有当前函数一个调用记录。

// 经典递归

const sum=(num)=>{
  console.trace()
  if(num===1) return num;
  return num + sum(num-1)
}

// 我们看一下堆栈记录
<!--
sum(5)
(5 + sum(4))
(5 + (4 + sum(3)))
(5 + (4 + (3 + sum(2))))
(5 + (4 + (3 + (2 + sum(1))))) (5 + (4 + (3 + (2 + 1))))
(5 + (4 + (3 + 3)))
(5 + (4 + 6))
(5 + 10)
15
-->
// 尾递归
const sum=(num,total=0)=>{
  console.trace()
  if(num===1) return num + total
  return sum(num - 1,num + total)
}

// 继续看堆栈记录
<!--
sum(5, 0)
sum(4, 5)
sum(3, 9)
sum(2, 12)
sum(1, 14)
15
-->

我们可以看到,对于经典递归,我们每次计算的值都需要在堆栈保存,而尾递归,调用一次后,会直接进入下一个栈中执行,相比于经典递归,内存更小,更能防止内存泄漏

终极优化递归:while/reduce

函数式编程基础知识

范畴论

  • 1.范畴,可以理解是一个容器,主要包含两样东西,值(value),值的变形关系(function)
  • 2.范畴论使用函数,表达范畴之间的关系
  • 3.随着范畴论的发展,衍生出一套函数的运算方法,就是如今的“函数式编程”
  • 4.前面也说到了纯函数,因为 函数式编程从范畴论发展而来,本质目的,就是 为了求值,所以 要求函数必须是纯的
  • 5.函数式编程中的不可变状态,也和范畴论有关,下面的函子会说到

函子(functor)

  • 1.从范畴论我们知道,函数可以表达 范畴间的关系,还可以将一个范畴变成另一个范畴,就是“函子”
  • 2.函子是函数式编程的数据类型,也是基本的运算单位,而且函子也是一种范畴,比较特殊的是,函子可以将当前容器变成另一个容器
  • 3.函子观念,就是当前范畴对自己的抽象,赋予当前容器(范畴)自己去调用函数的能力 ,把东西装入容器,留一个 map 出口,map 一个函数时,容器自己运行这个函数,以达到分时、分场景的操作这个函数,就是我们常说的一些特性,比如惰性求值、错误 处理、异步调用等等

函子的结构

  • 具有 map 方法,作用于容器内的每一个值,用于将容器里面的每一个值映射到另一个容器
  • 可以调用自身,把自身当做容器
  • 函子还有一个 of 方法,用于调用自身
// 基本函子
const container = function (x) {
  this._value = x;
}
container.of = x => new container(x)
container.prototype.map = function (f) {
  return container.of(f(this._value))
}
container.of(3)
  .map(x => x + 1)
  .map(x => x + 1)
  .map(x => console.log(x)) // 5

简单总结下

  • 函数式编程中的运算,都是通过函子来完成
  • 函子通过对外接口(map),引发容器中值的变形

上面这个函子是最简单的函子,接下来看其他几个基本的函子。。。

Maybe 函子

函子接受各种函数,处理容器内不的值,万一容器内部的值可能是一个空值,而不能与外部的值进行运算,那么很容易出错,这个时候,Maybe 函子就流产了


// basic functor
class Functor{
  constructor(value){
    this._value=value
  }
  of(value){
    return new Functor(value)
  }
  static map(f){
    return this.of(f(this._value))
  }
}
// Maybe functor

class Maybe extends Functor {
  map (f) {
    return this._value ? this.of(f(this._value)) : this.of(null)
  }
}

Either 函子

我们容器做的事情太少了,上面的 Maybe 函子虽然能解决一些固定情况,但是并不能处理所有错误,虽然我们可以使用 try…catch,但是 try…caatch 并不是纯的,紧接着,Either 函子就出生了


  • Either 函子的目的并不是为了处理所有的错误,更类似于 id…else 这样的常见逻辑运算
  • Either 内部主要有两个值,左值(left)和右值(right),左值是由值不存在使用的,右值是正常情况使用的
class Either extends Functor {
  constructor(left, right) {
    this.left = left;
    this.right = right;
  }
  map (f) {
    return this.right ? Either.of(this.left, f(this.right)) : Either.of(f(this.left), this.right)
  }
}
Either.of = function (left, right) {
  return new Either(left, right);
};

AP 函子

Ap 函子,就是为了解决我们容器中的值,有可能是函数的情况,简单介绍下

class Ap extends Functor {
  ap (F) {
    return Ap.of(this.val(F.val));
  }
}

IO 函子

上面说了这么多,纯函数、状态不可变等,但是在我们实际应用中,总是要去搬砖(业务),也有类似的术语,比如脏检查等


  • IO 函子与之前的一些函子有些不同,IO 函子的 value 是一个函数,这个函数会把不纯的操作(IO、网络请求、DOM 等)包裹在其中,延迟执行
  • IO 函子也可以说是惰性求值,因为它需要收集这些不纯的操作,延迟执行
  • IO 函子收集不纯的操作后,也增加了复杂性和不可维护性
import _ from 'lodash';
const compose = _.flowRight;
class IO extends Monad{
  map(f){
    return IO.of(compose(f, this.__value))
  }
}

现在,我们可以通过 IO 函子收集脏操作,并且通过函数组合的形式,执行了所有的脏操作

Monad 函子

Monad 其实也是一种设计模式,表示将一个运算过程,通过函数拆解成互相连接的多个步骤,依次运行下去 ,Promise 就是一种 Monad


  • Monad 解决了嵌套地狱,可以有效的出出力嵌套的Functor
  • 更加简单化容器中的值
  • Monad 函子,总是返回一个单层的函子
  • 一般包括一个flatMap方法,如果函子存在嵌套问题,它会取出最后内部的值,保证返回的永远是一个单层的容器
class Monad extends Functor {
  join() {
    return this.val;
  }
  flatMap(f) {
    return this.map(f).join();
  }
}

函子总结

看一段简单的代码,总结一下上面几个函子

// 用一下 lodash的砖

const fs = require("fs")

function flow (...funcs) {
  const length = funcs.length
  let index = length
  while (index--) {
    if (typeof funcs[index] !== 'function') {
      throw new TypeError('Expected a function')
    }
  }
  return function (...args) {
    let index = 0
    let result = length ? funcs[index].apply(this, args) : args[0]
    while (++index < length) {
      result = funcs[index].call(this, result)
    }
    return result
  }
}


function flowRight (...funcs) {
  return flow(...funcs.reverse())
}
// basic functor

class Functor {
  constructor(val) {
    this.val = val
  }
  map (f) {
    return new Functor(f(this.val))
  }
}

// Monad functor

class Monad extends Functor {
  join () {
    return this.val;
  }
  flatMap (f) {
    return this.map(f).join()
  }
}



const compose = flowRight;

class IO extends Monad {
  // 收集脏操作
  static of (val) {
    return new IO(val)
  }
  map (f) {
    return IO.of(compose(f, this.val))
  }
}

const readFile = function (fileName) {
  return IO.of(function () {
    return fs.readFileSync(fileName, 'utf-8')
  })
}

const print = (x) => {
  return IO.of(function () {
    return x + '🍊'
  })
}

const tail = function (x) {
  return IO.of(function () {
    return x + '🍺'
  })
}

const result = readFile('./user.txt')
  .flatMap(print)
  .flatMap(tail)

console.log(result().val())

实际应用—Redux


  • createStore 就是一个容器
  • state 值
  • action 变形关系
  • reducer map
  • middle IO函子
  • applyMiddle of
  • compose 函数组合

常用的一些函数式编程库

  • lodash
  • underscore
  • cycle
  • rxjs

结语

  • 函数式编程并不是灵丹妙药,并不一定非要去使用它,它带来了更高的函数组合性、容错、灵活性等
  • 函数式编程也和OOP一样,只是一种编程范式
  • OOP意旨降低复杂度、封装、继承、接口等定义,而函数式编程通过组合、柯里化、Functor等方式来降低编码系统的复杂度
0