1.在node开发中 基本上需要大量的高阶函数 2.在写react的时候需要经常构建一些高阶组件 在需要写有关于redux的时候需要构建一些纯函数再去组合 包括这些库的源码也是 3.再平时写业务的时候 需要把复用的函数抽象出纯函数给团队去用

# 函数式编程思维

# 范畴论

1、函数式编程是范畴论的数学分支是一门很复杂的数学,认为世界上所有概念体系都可以抽象出一个个范畴 2、彼此之前存在某种关系概念、事物、对象等等,都构成范畴。任何事物只要找出他们之间的关系,就能定义 3、箭头表示范畴成员之间的关系,正式的名称叫做‘态射’。范畴论认为,同一个范畴的所有成员,就是不同状态的‘变形’。通过‘态射’,一个成员可以变成另一个成员。

An image

# 函数式编程基础理论

1、函数式编程(Functional Programming)其实相对于计算机的历史 而言是一个非常古老的概念,甚至早于第一台计算机的诞生。函 数式编程的基础模型来源于 λ (Lambda x=>x*2)演算,而 λ 演算并 非设计于在计算机上执行,它是在 20 世纪三十年代引入的一套用 于研究函数定义、函数应用和递归的形式系统。

2、函数式编程不是用函数来编程,也不是传统的面向过程编程。主旨在于将复杂的函数符合成简单的函数(计算理论,或者递归论, 或者拉姆达演算)。运算过程尽量写成一系列嵌套的函数调用

3、JavaScript 是披着 C 外衣的 Lisp

4、真正的火热是随着React的高阶函数而逐步升温

5、函数是一等公民。所谓‘第一等公民’,指的是函数与其他数据类型一样,处于平等地位。可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。

6、不可改变量。在函数式编程中,我们通常理解的变量在函数式编程中也被函数代替了:在函数式编程中变量仅仅代表某个表达式。这里所说的‘变量’是不能被修改的。所有的变量只能被赋一次值。

7、map和reduce他们是最常用的函数式编程方法。

1、函数是一等公民 2、只用表达式,不用语句 3、没有副作用 4、不修改状态 5、引用透明(函数运行只靠参数)

# 函数式编程常用核心概念

# 纯函数

对于相同的输入,永远得到相同的输出,而且没有可观察的副作用,也不依赖外部环境的状态。 var xs = [1,2,3,4,5,6]; Array.slice是纯函数,因为它没有副作用,对于固定的输入,输出总是固定的 xs.slice(0,3) xs.splice(0,3)//这个就不是纯函数,因为它改变了原来的数组。

优缺点

import _ from 'lodash';
let sin = _.memorize(x => Math.sin(x));
//第一次计算的时候会稍微慢点
let a = sin(1);
//第二次有了缓存,速度极快
let b = sin(2);
1
2
3
4
5
6

纯函数不仅可以有效降低系统的复杂度,还有很多很棒的特性。比如:可缓存性

//不纯的

let min = 19;
let checkage = age => age > min;
1
2

//纯的 这很函数式

let checkage = age => age > 19;
1

在不纯的版本中,checkage不仅取决于age还有外部的变量min 纯的checkage把关键字18硬编码在函数内部,扩展性较差,柯里化优雅的函数式解决

# 函数的柯里化

传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数 我们一起用柯里化来改变它

let checkage = min => (age => age > min);
let checkage18 = checkage(18);
checkage18(20);
1
2
3

函数柯里化的code

//柯里化之前
function add(x,y) {
    return x + y;
}
add(1,2)

//柯里化之后
function addx(y) {
    return functuon(x) {
        return x + y;
    }
}
addx(1)(2)
1
2
3
4
5
6
7
8
9
10
11
12
13
function foo(p1,p2){
    this.val = p1 + p2;
}
var bar = foo.bind(null,'p1');
var baz = new bar('p2')
console.log(baz.val);//p1p2
1
2
3
4
5
6

# 函数组合

纯函数以及如何把它柯里化写出来的洋葱代码h(g(f(x))),为了解决函数嵌套的问题,我们需要用到‘函数组合’ 我们一起来用柯里化来改变他,让多个函数像拼积木一样。

const compose = (f,g) => (x => f(g(x)));
var first = arr => arr[0]var reverse = arr => arr.reverse();
var last = compose(first,reverse);
last([1,2,3,4,5])
1
2
3
4
5

An image

# Point Free

把一些对象自带的方法转化成纯函数,不要命名转瞬即逝的中间变量。 这个函数中,我们使用了str作为我们的中间变量,但是中间变量除了让代码变长了一点外毫无意义。

const f = str => str.toUpperCase().split('');

优缺点

var toUpperCase = world => word.toUpperCase();
var split = x => (str => str.split(x));
var f = compose(split(''),toUpperCase);
f('abcd efgh');
1
2
3
4

这种风格能够帮助我们减少不必要的命名,让代码保持简洁和通用。

# 声明式与命名式

命名式代码的意思就是,我们通过编写一条又一条指令让计算机执行一些动作,这其实一般都会涉及到很多繁杂的细节。 而声明式就优雅很多了,我们通过表达式的写法来声明我们想要干什么,而不是通过一步一步的指示。 //命名式

let CEOs = [];
for(var i = 0; i < companies.length; i++){
    CEOs.push(companies[i].CEO)
}
1
2
3
4

//声明式

let CEOs = companies.map(c => c.CEO);
1

优缺点 函数式编程的一个明显的好处就是这种声明式的代码,对于无副作用的纯函数,我们完全可以不考虑函数内部是如何实 现的,专注于编写业务代码。优化代码时,目光只需要集中在这些稳定坚固的函数内部即可。 相反,不纯的函数式的代码会产生副作用或者依赖外部系统环境,使用它们的时候总是要考虑这些不干净的副作用。在 复杂的系统中,这对于程序员的心智来说是极大的负担。

# 惰性求值、惰性函数、惰性链

惰性函数 惰性载入表示函数执行的分支只会在函数第一次掉用的时候执行,在第一次调用过程中,该函数会被覆盖为另一个按照合适方式执行的函数,这样任何对原函数的调用就不用再经过执行的分支了。

为了兼容各浏览器,对事件监听的的支持:

function addEvent (type, element, fun) {
    if (element.addEventListener) {
        element.addEventListener(type, fun, false);
    }
    else if(element.attachEvent){
        element.attachEvent('on' + type, fun);
    }
    else{
        element['on' + type] = fun;
    }
}
1
2
3
4
5
6
7
8
9
10
11

上面是注册函数监听的各浏览器兼容函数。由于,各浏览之间的差异,不得不在用的时候做能力检测。显然,单从功能上讲,已经做到了兼容各浏览器。但是,每次绑定监听,都会对能力做一次检测,这就没有必要了,真正的应用中,这显然是多余的,同一个应用环境中,只需要检测一次即可。

于是有了如下改变:

function addEvent (type, element, fun) {
    if (element.addEventListener) {
        addEvent = function (type, element, fun) {
            element.addEventListener(type, fun, false);
        }
    }
    else if(element.attachEvent){
        addEvent = function (type, element, fun) {
            element.attachEvent('on' + type, fun);
        }
    }
    else{
        addEvent = function (type, element, fun) {
            element['on' + type] = fun;
        }
    }
    return addEvent(type, element, fun);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

可以看出,第一次调用addEvent会对浏览器做能力检测,然后,重写了addEvent。下次再调用的时候,由于函数被重写,不会再做能力检测。

封装ajax同理

function ajax() {
    var xhr = null;
    if(window.XMLHttpRequset) {
        xhr = new XMLHttpRequset()
    } else {
        xhr = new ActiveXObject('Microsoft.XMLHTTP');
    }
    ajax = xhr;
    return xhr;
}
1
2
3
4
5
6
7
8
9
10

# 高阶函数

函数当参数,把传入的函数做一个封装,然后返回这个封装函数,达到更高程度的抽象。 //命名式

var add = function(a,b) {
    return a + b;
};
function math(func,array) {
    return func(array[0],array[1]);
}

math(add,[1,2]);//3
1
2
3
4
5
6
7
8

# 尾调用优化

指函数内部的最后一个动作是函数调用。该调用的返回值,直接返回给函数。。 函数调用自身,称为递归。如果尾调用自身,就称为尾递归。递归需要保存大 量的调用记录,很容易发生栈溢出错误,如果使用尾递归优化,将递归变为循 环,那么只需要保存一个调用记录,这样就不会发生栈溢出错误了。

递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。

function factorial(n) {
  if (n === 1) return 1;
  return n * factorial(n - 1);
}

factorial(5) // 120
1
2
3
4
5
6

上面代码是一个阶乘函数,计算n的阶乘,最多需要保存n个调用记录,复杂度 O(n) 。

如果改写成尾递归,只保留一个调用记录,复杂度 O(1) 。

function factorial(n, total) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}

factorial(5, 1) // 120
1
2
3
4
5
6

An image

An image

An image 浏览器并没有实现尾递归调用优化,为了调试报错。

An image

An image

# 闭包

闭包缓存了所在作用域的词法作用域 缓存了上下文执行环境的词法作用域 An image

# 范畴与容器

1.我们可以把”范畴”想象成是一个容器,里面包含两样东西。值(value)、值的变形关系,也就是函数。 2.范畴论使用函数,表达范畴之间的关系。 3.伴随着范畴论的发展,就发展出一整套函数的运算方法。这套方法起初只用于数学运算,后来有人将它在计算机上实现了,就变成了今 天的”函数式编程"。 4.本质上,函数式编程只是范畴论的运算方法,跟数理逻辑、微积分、 行列式是同一类东西,都是数学方法,只是碰巧它能用来写程序。为什么函数式编程要求函数必须是纯的,不能有副作用?因为它是一种数学运算,原始目的就是求值,不做其他事情,否则就无法满足函数 运算法则了。

An image

函子是个啥一定要搞清楚

# 容器、Functor(函子)

# 容器

$(...) 返回的对象并不是一个原生的 DOM 对象,而是对于原生对象的一种封装,这在某种意义上就是一个“容器”(但它并不函数式)。

# Functor(函子)

Functor(函子)遵守一些特定规则的容器类型。 Functor 是一个对于函数调用的抽象,我们赋予容器自己去调用函数的能力。把东西装进一个容器,只留出一个接口 map 给容器外的函数,map一个函数时,我们让容器自己来运行这个函数, 这样容器就可以自由地选择何时何地如何操作这个函数,以致于拥有惰性求值、错误处理、异步调用等等非常牛掰的特性。

//很明显看单词 这是一个容器
var Container = function(x) {
    this.__value = x; 
}

//函数式编程一般约定,函子有一个of方法 这个of方法很明显是自己定义的
Container.of = x => new Container(x);

//一般约定,函子的标志就是容器具有map方法。该方法将容器 里面的每一个值,映射到另一个容器。 
//这很明显是函子,拥有map方法,map里面传的就是上图的f(变形关系)
Container.prototype.map = function(f){
    //这里为什么通过of不直接用new? 是为了区别面向对象
    return Container.of(f(this.__value)) 
}
Container.of(3)
    .map(x => x + 1) //Container(4)
    .map(x => 'Result is ' + x); //Container('Result is 4')
//生成了3个函子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

ES6实现

class Functor{
    constructor(val) {
        this.val = val;
    }
    map(f) {
        return new Functor(f(this.val))
    }
}
(new Functor(5)).map((num) => num + 2);
//Functor {val: 7}
1
2
3
4
5
6
7
8
9
10
# map

上面代码中,Functor是一个函子,它的map方法接受函数f作为参数,然后返回一个新的函子,里面包含的值是被f处理过的 (f(this.val))。 一般约定,函子的标志就是容器具有map方法。该方法将容器里 面的每一个值,映射到另一个容器。 上面的例子说明,函数式编程里面的运算,都是通过函子完成, 即运算不直接针对值,而是针对这个值的容器----函子。函子本身具有对外接口(map方法),各种函数就是运算符,通过接口接入容器,引发容器里面的值的变形。 因此,学习函数式编程,实际上就是学习函子的各种运算。由于可以把运算方法封装在函子里面,所以又衍生出各种不同类型的函子,有多少种运算,就有多少种函子。函数式编程就变成了运用不同的函子,解决实际问题。

# Maybe 函子(if)

函子接受各种函数,处理容器内部的值。这里就有一个问题,容器内部的值可能是一个空值(比如null),而外部函数未必有处理空值的机制,如果传入空值,很可能就会出错。

ES5实现

var Maybe = function(x) { 
    this.__value = x;
}
Maybe.of = function(x) {
    return new Maybe(x); 
}
Maybe.prototype.map = function(f) {
    return this.isNothing() ? Maybe.of(null) : Maybe.of(f(this.__value));
}
Maybe.prototype.isNothing = function() {
    return (this.__value === null || this.__value === undefined); 
}
1
2
3
4
5
6
7
8
9
10
11
12

ES6实现

Functor.of(null).map(function (s) { 
    return s.toUpperCase();
});
// TypeError
class Maybe extends Functor {
    map(f) {
        return this.val ? Maybe.of(f(this.val)) : Maybe.of(null);
    } 
}
Maybe.of(null).map(function (s) { 
    return s.toUpperCase();
});
// Maybe(null)
1
2
3
4
5
6
7
8
9
10
11
12
13
# Either

An image An image

# AP

An image An image

# IO

1.真正的程序总要去接触肮脏的世界。

function readLocalStorage(){
    return window.localStorage;
}
1
2
3

2.IO 跟前面那几个 Functor 不同的地方在于,它的 __value 是一个函数。 它把不纯的操作(比如 IO、网络请求、DOM)包裹到一个函数内,从而 延迟这个操作的执行。所以我们认为,IO 包含的是被包裹的操作的返回 值。 3.IO其实也算是惰性求值。 4.IO负责了调用链积累了很多很多不纯的操作,带来的复杂性和不可维护性

import _ from 'lodash'; 
var compose = _.flowRight;
var IO = function(f) {
    this.__value = f; 
}
IO.of = x => new IO(_ => x);
IO.prototype.map = function(f) {
    return new IO(compose(f, this.__value)) 
};

import _ from 'lodash'; 
var compose = _.flowRight;
class IO extends Monad{ 
    map(f){
        return IO.of(compose(f, this.__value)) 
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

IO函子

var fs = require('fs');
var readFile = function(filename) {
    return new IO(function() {
        return fs.readFileSync(filename, 'utf-8');
    }); 
};
readFile('./user.txt') 
.flatMap(tail) 
.flatMap(print)
// 等同于 readFile('./user.txt') .chain(tail) .chain(print)
1
2
3
4
5
6
7
8
9
10
# Monad

avatar

1.Monad就是一种设计模式,表示将一个运算过程,通过 函数拆解成互相连接的多个步骤。你只要提供下一步运算 所需的函数,整个运算就会自动进行下去。 2.Promise 就是一种 Monad。 3.Monad 让我们避开了嵌套地狱,可以轻松地进行深度嵌套的函数式编程,比如IO和其他异步编程。 4.记得让上面的IO集成Monad

avatar

var fs = require('fs');
var _ = require('lodash');
//基础函子
class Functor {
    constructor(val) {
        this.val = val;
    }
    map(f) {
        return new Functor(f(this.val));
    }
}
//Monad 函子
class Monad extends Functor {
    join() {
        return this.val;
    }
    flatMap(f) {
        //1.f == 接受一个函数返回的事IO函子
        //2.this.val 等于上一步的脏操作
        //3.this.map(f) compose(f, this.val) 函数组合 需要手动执行
        //4.返回这个组合函数并执行 注意先后的顺序
        return this.map(f).join();
    }
}
var compose = _.flowRight;
//IO函子用来包裹📦脏操作
class IO extends Monad {
    //val是最初的脏操作
    static of (val) {
        return new IO(val);
    }
    map(f) {
        return IO.of(compose(f, this.val))
    }
}
var readFile = function (filename) {
    return IO.of(function () {
        return fs.readFileSync(filename, 'utf-8');
    });
};
var print = function (x) {
    console.log("🍊");
    return IO.of(function () {
        console.log("🍎")
        return x + "函数式";
    });
}
var tail = function (x) {
    console.log(x);
    return IO.of(function () {
        return x+"【京程一灯】";
    });
}
const result = readFile('./user.txt')
    //flatMap 继续脏操作的链式调用
    .flatMap(print);
    // .flatMap(print)()
    // .flatMap(tail)();
// console.log(result);
console.log(result().val());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

# 当下函数式编程最热的库

1、RxJS frp => angular 2、cycleJS 3、lodashJS、lazy(惰性求值) 4、underscoreJS 5、ramdajs

# 函数式编程的实际应用场景

# 易调试、热部署、并发

# 单元测试

严格函数式编程的每一个符号都是对直接量或者表达式结果的引用, 没有函数产生副作用。因为从未在某个地方修改过值,也没有函数修 改过在其作用域之外的量并被其他函数使用(如类成员或全局变量)。 这意味着函数求值的结果只是其返回值,而惟一影响其返回值的就是 函数的参数。 这是单元测试者的梦中仙境(wet dream)。对被测试程序中的每个函数, 你只需在意其参数,而不必考虑函数调用顺序,不用谨慎地设置外部 状态。所有要做的就是传递代表了边际情况的参数。如果程序中的每 个函数都通过了单元测试,你就对这个软件的质量有了相当的自信。 而命令式编程就不能这样乐观了,在 Java 或 C++ 中只检查函数的返 回值还不够——我们还必须验证这个函数可能修改了的外部状态。

# 总结与补充

函数式编程不应被视为灵丹妙药。相反,它应 该被视为我们现有工具箱的一个很自然的补充 —— 它带来了更高的可组合性,灵活性以及容错 性。现代的JavaScript库已经开始尝试拥抱函数式编 程的概念以获取这些优势。Redux 作为一种 FLUX 的变种实现,核心理念也是状态机和函数式编程。 软件工程上讲『没有银弹』,函数式编程同样也不是万能的,它与烂大街的 OOP 一样,只是一种编程范式而已。很多实际应用中是很难用函数式去表达的,选择 OOP 亦或是其它编程范式或许会更简单。但我们要注意到函数式编程的核心理念, 如果说 OOP 降低复杂度是靠良好的封装、继承、多态以及接口定义的话,那么函 数式编程就是通过纯函数以及它们的组合、柯里化、Functor 等技术来降低系统复 杂度,而 React、Rxjs、Cycle.js 正是这种理念的代言。让我们一起拥抱函数式编程, 打开你程序的大门!