howieyi

No pains, No gains!

View project on GitHub

函数式编程深入理念

范畴与容器:函数式编程只是范畴论的运算方法,跟数理逻辑、微积分、 行列式是同一类东西,都是数学方法,只是碰巧它能用来写程序。为 什么函数式编程要求函数必须是纯的,不能有副作用?因为它是一种 数学运算,原始目的就是求值,不做其他事情,否则就无法满足函数 运算法则了。

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

  • 容器、Functor(函子)

  • 函子的标志就是容器具有 map 方法。该方法将容器里 面的每一个值,映射到另一个容器。

  • 函数式编程里面的运算,都是通过函子完成, 即运算不直接针对值,而是针对这个值的容器—-函子。函子本 身具有对外接口(map 方法),各种函数就是运算符,通过接口 接入容器,引发容器里面的值的变形

<!-- 容器、Functor(函子) -->
var Container = function(x){
    this.__value = x;
}
// 函数式编程一般约定,函子有一个of方法
Container.of = x => new Container(x);

// Container.of(‘abcd’);
// 一般约定,函子的标志就是容器具有map方法。
// 该方法将容器 里面的每一个值,映射到另一个容器。
Container.prototype.map = function(f){
    return Container.of(f(this.__value));
}

Container.of(3)
    .map(x => x + 1) //=> Container(4)
    .map(x => 'Result is ' + x); //=> Container('Result is 4')
  • map
class Functor {
    constructor(val) {
        this.val = val;
    }

    map(f) {
        // Functor是一个函子,它的map方法接受函数f作为 参数,然后返回一个新的函子,里面包含的值是被f处理过的 (f(this.val))。
        return new Functor(f(this.val))
    }
}

(new Functor(2)).map(function(two) {
    return two + 2;
}); // Functor(4)
  • of: 函数式编程一般约定,函子有一个 of 方法,用来生成新 的容器。
Functor.of = function(val) {
    return new Functor(val);
}

Functor.of(2).map(function(two) {
    return two + 2;
}); // Functor(4)

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

Functor(null).map(function(s){
    return s.toUpperCase();
}); // TypeError

<!-- Maybe 函子 es6 class -->
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)

<!-- Maybe 函子 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
}

错误处理、Either

  • try/catch/throw 并不是 “纯”的,因为它从外部接管了我们的函数,并且在这个 函数出错时抛弃了它的返回值。
  • Promise 是可以调用 catch 来集中处理错误的。
  • 事实上 Either 并不只是用来作错误处理的,它表示了逻辑或,范畴学里的 coproduc。
var Left = function(x){
    this._value = x;
}
var Right = function(x){
    this._value = x;
}
Left.of = function(x){
    return new Left(x);
}
Right.of = function(x){
    return new Right(x);
}

<!-- 区别处理 -->
// 不会对容器做任何事情,只是很简单地把这个容器拿进来又扔出去。 这个特性意味着,Left 可以用来传递一个错误消息
Left.prototype.map = function(f){
    return this;
}
Right.prototype.map = function(f){
    return Right.of(f(this._value));
}

<!-- 逻辑处理 -->
var getAge = user => user.age ? Right.of(user.age) : Left.of('Error');

getAge({name: 'stark', age: 22}).map(age => 'Age is ' + age); // Right('Age is 21')

// Left 可以让调用链中任意一环的错误立刻返回到调用链的尾部, 这给我们错误处理带来了很大的方便,再也不用一层又一层的 try/catch
getAge({name: 'stark'}).map(age => 'Age is ' + age); // Left('Error')

AP 因子:函子里面包含的值,完全可能是函数。我们可以想象 这样一种情况,一个函子的值是数值,另一个函子的值 是函数

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

IO:IO 跟前面那几个 Functor 不同的地方在于,它的 __value 是一个函数。 它把不纯的操作(比如 IO、网络请求、DOM)包裹到一个函数内,从而 延迟这个操作的执行。所以我们认为,IO 包含的是被包裹的操作的返回 值。

  • IO 其实也算是惰性求值
  • IO 负责了调用链积累了很多很多不存的操作,带来的复杂性和不可维护性
import _ from 'lodash';
var compose = _.flowRight;

<!-- es5 -->
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));
}

<!-- es6 -->
class IO extends Monad {
    map(f){
        return IO.of(compose(f,this.__value));
    }
}
  • IO 函子
var fs = require('fs');
var readFile = function(filename) {
    return new IO(function() {
        return fs.readFileSync(filename, 'utf-8');
    });
};

Monad:Monad 就是一种设计模式,表示将一个运算过程,通过 函数拆解成互相连接的多个步骤。你只要提供下一步运算 所需的函数,整个运算就会自动进行下去。

  • Promise 就是一种 Monad
  • Monad 让我们避开了嵌套地狱,可以轻松地进行深度嵌套的函数式编程,比如 IO 和其他异步任务
  • Monad 函子的作用是,总是返回一个单层的函子。它有一个 flatMap 方法,与 map 的方法作用相同,唯一的区别是如果生成了一个嵌套函子,它会取出后者内部的值,保证返回的永远是一个层的容器,不会出现嵌套的情况
Maybe.of(
    Maybe.of(
        Maybe.of({name: 'sdasd', age: '14'})
    )
)

<!-- Monad -->
class Monad extends Functor {
    join(){
        return this.val;
    }

    // 如果函数f返回的是一个函子,那么this.map(f)就会生成一个嵌套的函子。所以,join方法保证了 flatMap 方法总是返回一个单层的函子。这意味着嵌套的函子会被铺平
    flatMap(f) {
        return this.map(f).join();
    }
}

易调试、热部署、并发

  • 函数式编程中的每个符号都是 const 的,于是没有什么函数会有副作用。 谁也不能在运行时修改任何东西,也没有函数可以修改在它的作用域之外修 改什么值给其他函数继续使用。这意味着决定函数执行结果的唯一因素就是 它的返回值,而影响其返回值的唯一因素就是它的参数。
  • 函数式编程不需要考虑”死锁”(deadlock),因为它不修改变量,所以根本 不存在”锁”线程的问题。不必担心一个线程的数据,被另一个线程修改,所 以可以很放心地把工作分摊到多个线程,部署”并发编程”(concurrency)。
  • 函数式编程中所有状态就是传给函数的参数,而参数都是储存在栈上的。 这一特性让软件的热部署变得十分简单。只要比较一下正在运行的代码以及 新的代码获得一个 diff,然后用这个 diff 更新现有的代码,新代码的热部署就 完成了

流行的几大函数式编程库

  • rxjs: 所有的外部输入(用户输入、网络请求等等)都被视作一 种 『事件流』
  • cycle: 一个基于 Rxjs 的框架,它是一个彻彻底底的 FRP 理念的框架, 和 React 一样支持 virtual DOM、JSX 语法,但现在似乎还没有看到大型 的应用经验。
  • loadsh: 一个 JavaScript 工具库,它提供了一整套函数式编程 的实用功能,但是没有扩展任何 JavaScript 内置对象
  • lazy(惰性求值)
  • underscore: 一个具有一致接口、模块化、高性能等特性的 JavaScript 工 具库,是 underscore.js 的 fork,其最初目标也是“一致的跨浏览器行 为。。。,并改善性能”。lodash 采用延迟计算,意味着我们的链式方法在显式或者隐式的 value()调用之前是不会执行的,因此 lodash 可以进行 shortcut(捷 径) fusion(融合)这样的优化,通过合并链式大大降低迭代的次 数,从而大大提升其执行性能。
  • ramda.js: 一个非常优秀的 js 工具库
    • ramda 里面的提供的函数全部都是 curry 的 意味着函数没有默认参 数可选参数从而减轻认知函数的难度。
    • ramda 推崇 pointfree 简单的说是使用简单函数组合实现一个复杂 功能,而不是单独写一个函数操作临时变量。
    • ramda 有个非常好用的参数占位符 R._ 大大减轻了函数在 pointfree 过程中参数位置的问题