什么是函数式编程(Functional Programming)?
带着这个疑问搜了下。
函数式编程属于一种声明式的编程范式。
而且对于Javascript这门语言
函数是一等“公民”
纯函数是这样一种函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用。
这个思想我们在React Hooks的设计上能够清楚的了解和认识它。
在函数式编程中,函数就是一个管道(pipe)。这头进去一个值(参数),那头就会返回一个计算后的值(返回值),没有其它副作用。而且只要参数一样,返回值也是一样的。
一、基础概念
函数式编程其实是一种数学运算方法,当然后来也可以用来做一种编程范式的思想。它起源于一门叫做范畴论(Category Theory)的数学分支。
范畴论 这部分只简单了解部分概念即可,后面有部分概念的使用。
彼此之间存在某种关系的概念、事务等,都构成了“范畴”。对象(object)的搜集、态射的搜集、态射的组合都能形成一个“范畴”。 箭头表示范畴成员之间的关系,即“态射”(morphism)。 同一个范畴的所有成员,就是不同状态的变换(transformation)。
二、函数的合成和柯里化
函数式编程的两个最基本的运算
合成 柯里化
函数的合成
如果一个值要经过多个函数处理,最后得到返回值,就可以把中间的函数合并成一个函数,这就叫“函数的合成”(compose)。
var compose = function(f,g) {
return function(x) {
return f(g(x));
};
};
f 和 g 都是函数,x 就是输入的参数,在它们之间通过“管道”传输的值。f 和 g 组合的函数就是“函数的合成”。
函数的合成必须满足结合律
compose(f, compose(g, h))
compose(compose(f, g), h)
compose(f, g, h)
这个也就要求函数必须是“纯”的原因了。
pointfree模式:组合函数内部不需要提及将要操作的数据是什么
// 非 pointfree模式,因为提到了数据:name
var initials = function (name) {
return name.split(' ').map(compose(toUpperCase, head)).join('. ');
};
// pointfree 模式
var initials = compose(
join('. '),
map(compose(toUpperCase, head)),
split(' ')
);
initials("hunter stockton thompson"); // 'H. S. T'
柯里化(Curry)
f(x)和g(x)合成为f(g(x)),有一个隐藏的前提是f 和 g 都接受一个参数。如果要接受多个参数,就需要使用函数的柯里化: 将多参数的函数转化为单参数的函数。
// 柯里化之前
function add(x, y) {
return x + y;
}
add(1, 2) // 3
// 柯里化之后
function addX(y) {
return function (x) {
return x + y;
};
}
var addTen = addX(10)
addX(2)(1) // 3
addTen(1) // 11
三、函子
函数不仅可以在同一个范畴中的值转换,还可以将一个范畴转换为另一个范畴。这就用到了函子(functor)。
class Functor {
constructor(val) {
this.val = val;
}
map(f) {
return new Functor(f(this.val));
}
}
// const Functor = val => { this._val = val; }
// Functor.prototype.map = f => new Functor(f(this._val));
上面代码中,Functor就是一个函子。
一般约定,实现了Map方法的容器是一个函子的标志。
functor 就是一个签了合约的接口。我们本来可以简单地把它称为 Mappable
但是要生成一个这样的函子,需要先使用new实例化,这就不太好看了。
所以又约定函子有一个of方法,用来生成新的容器。
Functor.of = val => new Functor(val);
Maybe函子
函子里的map方法遇到null或undefined会报错, Maybe函子就是为了解决这个问题的
const Maybe = val => { this._val = val; }
Maybe.of = val => new Maybe(val);
Maybe.prototype.isNothing = () => this._val
Maybe.prototype.map = f =>
this.isNothing() ? Maybe.of(f(this._val)) : Maybe.of(null);
Either 函子
Either 函子内部有两个值,左值(Left)和右值(Right),右值是正常情况的下使用的值,右值不正确时使用左值。
const Either = (left, right) => {
this.left = left;
this.right = right;
}
Either.of = (left, right) => new Either(left, right);
Either.prototype.map = f => this.right
? Either.of(this.left, f(this.right))
: Either.of(f(this.left), this.right);
Either 函子的常见用途是提供默认值
ap函子
ap函子的特点是一个函子的值是数值,一个函子的值是函数
Ap.prototype.map = F => Ap.of(this.val(F.val));
ap方法的参数不是函数,而是另一个函子 ap 函子的意义在于,对于那些多参数的函数,就可以从多个容器之中取值,实现函子的链式操作。
function add(x) {
return function (y) {
return x + y;
};
}
Ap.of(add).ap(Maybe.of(2)).ap(Maybe.of(3));
Monad函子
Monad 函子的重要应用,就是实现 I/O (输入输出)操作。
var fs = require('fs');
var readFile = function(filename) {
return new IO(function() {
return fs.readFileSync(filename, 'utf-8');
});
};
var print = function(x) {
return new IO(function() {
console.log(x);
return x;
});
}
var cat = compose(map(print), readFile);
cat(".git/config")
// IO(IO("[core]\nrepositoryformatversion = 0\n"))
从上可以看到IO嵌套一个IO,显得很奇怪,获取内部的值需要多次循环获取。
我们使用join方法将多层嵌套压成单层(flat),类似于多维数组转化为一维数组。
var ioio = IO.of(IO.of("pizza"));
// IO(IO("pizza"))
ioio.join()
// IO("pizza")
一个既定义了of方法又定义了join方法的算子,就叫Monad算子。Monad函子主要解决嵌套函子的取值问题。
Monad.prototype.join => this._val;
Monad.prototype.flatMap => this.map(f).join();
使用Monad函子进行IO操作变得更加简便(IO函子是一个Monad函子)
class IO extends Monad;
readFile('./user.txt')
.flatMap(print)
由于返回还是 IO 函子,所以可以实现链式操作。在大多数库里面,flatMap方法被改名成chain。
四、Ramda.js
和Underscore 和 Lodash 一样, Ramda也是JavaScript的一个工具库。和前两者最大的不同,Ramda专为函数编程风格而设计的,所有方法都支持柯里化。 柯里化
const addFour = (a, b, c, d) => a + b + c + d;
const curriedAddFour = R.curry(addFour);
const f = curriedAddFour(1, 2);
f(3)(4); //=> 10 等价于 curriedAddFour(1,2,3,4)或curriedAddFour(1)(2)(3)(4)
组合
const f = R.compose(Math.abs, R.add(1), R.multiply(2));
f(-4); // 7