技巧-函数式编程
基础概念
纯函数
纯函数: 对于相同的输入, 永远得到相同的输出, 并且没有任何可观察的副作用, 也不依赖外部环境的状态.
//不纯的
var min = 18;
var checkage = age => age > min;
//纯的,这很函数式
var checkage = age => age > 18;
纯函数的好处有很多, 比如我们可以对函数结果进行缓存
柯里化
柯里化: 传递给函数一部分参数来调用它, 让他返回一个函数去处理剩下的参数
实际上, 柯里化是一种"预加载"函数的方法, 通过传递比较少的参数, 得到一个已经记住了这些参数的新函数, 某种意义上, 这是一种对参数的缓存, 是一种非常高效的编写函数的方法
import { curry } from 'lodash';
//首先柯里化两个纯函数
var match = curry((reg, str) => str.match(reg));
var filter = curry((f, arr) => arr.filter(f));
//判断字符串里有没有空格
var haveSpace = match(/\s+/g);
haveSpace("ffffffff");
//=>null
haveSpace("a b");
//=>[" "]
filter(haveSpace, ["abcdefg", "Hello World"]);
//=>["Hello world"]
实现一个简单的柯里化如下
/**
* 将函数柯里化
* @param {*} fn 待柯里化的函数
* @param {*} len 所需的参数个数, 默认为原函数的形参的个数
* @returns
*/
function curry(fn, len = fn.length) {
return _curry.call(this, fn, len);
}
/**
* 中转函数
* @param {*} fn 待柯里化的原函数
* @param {*} len 所需的参数个数
* @param {...any} args 已接收的参数列表
*/
function _curry(fn, len, ...args) {
// 当前函数调用的时候传入的参数
return function (...params) {
let _args = [...args, ...params];
if (_args.length >= len) {
return fn.apply(this, _args);
} else {
// 参数还不够, 将已有的参数保存, 返回一个已经接受这些函数的新的函数
return _curry.call(this, fn, len, ...args);
}
};
}
let _fn = curry(function (a, b, c, d, e) {
console.log(a, b, c, d, e);
});
_fn(1, 2, 3, 4, 5); // print: 1,2,3,4,5
_fn(1)(2)(3, 4, 5); // print: 1,2,3,4,5
_fn(1, 2)(3, 4)(5); // print: 1,2,3,4,5
_fn(1)(2)(3)(4)(5); // print: 1,2,3,4,5
函数组合
在函数式编程中, 由于我们大量的使用函数进行逻辑操作, 会很容易写出包菜式的代码:
h(g(f(x)));
这看起来并不优雅, 为了解决这个问题, 我们需要借助一个工具方法: compose. 其最简单的实现如下:
//两个函数的组合
var compose = function(f, g) {
return function(x) {
return f(g(x));
};
};
//或者
var compose = (f, g) => (x => f(g(x)));
var add1 = x => x + 1;
var mul5 = x => x * 5;
compose(mul5, add1)(2);
// =>15
compose就像一个链接剂, 将两个纯函数结合在一起.
这种灵活的组合可以让我们拼接出任意复杂的函数
Point Free
观察之前的代码, 你会发现, 我们总是喜欢把一些对象自带的方法转化成纯函数:
var map = (f, arr) => arr.map(f);
var toUpperCase = word => word.toUpperCase();
这么做是有原因的, 这种风格大致的意思是: 不要命名转瞬即逝的中间变量:
//这不Piont free
var f = str => str.toUpperCase().split(' ');
这个函数中,我们使用了str作为我们的中间变量,但这个中间变量除了让代码变得长了一点以外是毫无意义的。下面改造一下这段代码:
var toUpperCase = word => word.toUpperCase();
var split = x => (str => str.split(x));
var f = compose(split(' '), toUpperCase);
f("abcd efgh");
// =>["ABCD", "EFGH"]
这种风格能够帮助我们减少不必要的命名, 让代码保持简介和通用. 当然, 为了在一些函数中写出Point Free风格的代码, 一定会有其他地方不那么的Point Free.
声明式与命令式
命令式代码是指: 我们通过编写一条又一条的指令让计算机执行一些动作, 这其中一般会涉及到很多比较复杂的细节.
而声明式意思是我们通过写表达式的方式来声明我们想要做什么, 而不是通过一步一步的指令.
//命令式
var CEOs = [];
for(var i = 0; i < companies.length; i++){
CEOs.push(companies[i].CEO)
}
//声明式
var CEOs = companies.map(c => c.CEO);
函数式编程的一个明显的好处就是这种声明式的代码,对于无副作用的纯函数,我们完全可以不考虑函数内部是如何实现的,专注于编写业务代码。优化代码时,目光只需要集中在这些稳定坚固的函数内部即可。
函子
- 函数式编程的运算不直接操作值, 而是由函子完成的
- 函子就是一个实现了map契约的对象
MayBe 函子
maybe函子用于处理在编程过程中遇到的一些错误.
const maybe = Maybe.of('hello world')
.map((x) => x.toUpperCase())
.map((x) => null)
.map((x) => x.split(' '));
maybe; // Maybe { _val: null }
虽然, 我们可以对不合法的空值已经进行了处理, 但是目前没有办法准确判断到底在什么地方传入了空值.
但是我们可以通过Either函子所解决.
Either 函子
Either, 顾名思义, 类似if...else, 可以经一部减少异常处理对函数造成的不可控性, 将副作用局限在一个可控的范围之内.
IO 函子
对于之前的函子来说, value的值是一个具体的数值, 但是得对于IO函子, 也可以把函数作为值来处理, 也就是说, 把会引起副作用的函数保存到value中进行延迟处理, 从而能够继续的控制副作用, 对函数进行提纯.
Task 函子
Task函子用来解决异步的回调低于的问题, 剑雨异步操作的执行比较麻烦, 所以案例中使用了Floktale中的task进行演示.
Folktale是一个支持函数式编程的库, 最大的优先在于:
- 组合型
- 更好的错误处理
- 更安全的同步, 通过Task实现
它没有提供很多功能性的函数, 更多注重的是函数式处理的操作, 比如compose, curry, task functor, either functor, maybe functor
Pointed函子
Pointed函子, 是实现了of静态方法的函子.
上面的所有函子, 之哟啊实现了静态的of方法, 就叫做Pointed函子.
of的作用有两个:
- 避免使用new来创造对象
- 把值放到上下文Context中
这就是说, 将值放到容器中, 利用map来处理.
利用map来处理数据是之前就讲到了, 执行上下文则是另一个概念.
执行上下文, 就是一个函数包括它自己作用域的环境
也就是说, 每次调用map函数, 每次调用map函数都会调用of, 创造一个新的作用域, 从而通过作用域链能够找到最近的函数来执行,
这也是of最主要的作用.
Monad 函子
在IO函子中, 一旦IO函子出现层级化的嵌套, 对于取值来说就会造成一定程度上的困难. 而Monad函子就是可以扁平化符复杂结构的Pointed函子.
Monad函子是具有join和of两个方法, 并遵守定律的函子.