函数式编程,起源于数学分支的范畴论,范畴论认为,所有概念体系都可以抽象出一个个范畴,任何事物只要找出他们之间的联系,就能定义。
同一个范畴的成员,就是不用状态的“transformation”,通过“态射”,一个成员可以变形为另一个成员。
对于相同的输入,会得到相同的输出,而且没有任何副作用,不依赖于外部状态
优缺点
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)))),为了解决函数嵌套的问题,我们要用到函数组合
经典案例
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])
函数组合,不只是可以直接组合所有函数,也可以拆开组合,相对于业务来说,也是一种扩展
compose(f,compose(g,h))
compose(compose(f,g),h)
compose(f,g,h)
抛出概念:组合子
** 这块还有很多没掌握,GG
目的就是将对象自带的方法转换成纯函数,在执行函数时,去掉不必要的命名
这段代码,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()
函数当参数,把传入的函数封装,返回这个封装的函数,让这个函数达到更高的抽象度
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
函子的结构
// 基本函子
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
简单总结下
上面这个函子是最简单的函子,接下来看其他几个基本的函子。。。
函子接受各种函数,处理容器内不的值,万一容器内部的值可能是一个空值,而不能与外部的值进行运算,那么很容易出错,这个时候,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)
}
}
我们容器做的事情太少了,上面的 Maybe 函子虽然能解决一些固定情况,但是并不能处理所有错误,虽然我们可以使用 try…catch,但是 try…caatch 并不是纯的,紧接着,Either 函子就出生了
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 函子,就是为了解决我们容器中的值,有可能是函数的情况,简单介绍下
class Ap extends Functor {
ap (F) {
return Ap.of(this.val(F.val));
}
}
上面说了这么多,纯函数、状态不可变等,但是在我们实际应用中,总是要去搬砖(业务),也有类似的术语,比如脏检查等
import _ from 'lodash';
const compose = _.flowRight;
class IO extends Monad{
map(f){
return IO.of(compose(f, this.__value))
}
}
现在,我们可以通过 IO 函子收集脏操作,并且通过函数组合的形式,执行了所有的脏操作
Monad 其实也是一种设计模式,表示将一个运算过程,通过函数拆解成互相连接的多个步骤,依次运行下去 ,Promise 就是一种 Monad
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