JS函数式编程【译】5.3 单子 (Monad)

单子是帮助你组合函数的工具。

像原始类型一样,单子是一种数据结构,它可以被当做装载让函子取东西的容器使用。 函子取出了数据,进行处理,然后放到一个新的单子中并将其返回。

我们将要关注三种单子:

  • Maybes
  • Promises
  • Lenses

除了用于数组的map和函数的compose以外,我们还有三种函子(maybe、promise和lens)。 这仅仅是另一些函子和单子。

Maybe

Maybe可以让我们优雅地使用有可能为空并且有默认值的数据。maybe是一个可以有值也可以没有值的变量,并且这对于调用者来说无所谓。

就他自己来说,这看起来不是什么大问题。所有人都知道空值检查可以通过一个if-else语句很容易地实现。

if (getUsername() == null) {
  username = 'Anonymous'
} else {
  username = getUsername();
}

但是用函数式编程,我们要打破过程的、一行接一样的做事方式,而应该用函数和数据的管道方式。 如果我们不得不从链的中间断开来检查值是否存在,我们就得创建临时变量并写更多的代码。 maybe仅仅是帮助我们保持逻辑跟随管道的工具。

要实现maybe,我们首先要创建一些构造器。

// Maybe单子构造器,目前是空的
var Maybe = function(){};

// None实例, 对一个没有值的对象的包装
var None = function(){};
None.prototype = Object.create(Maybe.prototype);
None.prototype.toString = function(){return 'None';};

// 现在可以写`none`函数了
// 这让我们不用总去写`new None()`
var none = function(){return new None()};

// Just实例, 对一个有一个值的对象的包装
var Just = function(x){return this.x = x;};
Just.prototype = Object.create(Maybe.prototype);
Just.prototype.toString = function(){return "Just "+this.x;};
var just = function(x) {return new Just(x)};

最后,我们可以写maybe函数了。它返回一个新的函数,这个函数返回“没东西”或者maybe。它是个函子。

var maybe = function(m) {
  if (m instanceof None) {
    return m;
  } else if (m instanceof Just) {
    return just(m.x);
  } else {
    throw new TypeError("Error: Just or None expected, " +
      m.toString() + " given.");
  }
}

我们还可以生成一个函子的生成器,就像数组的那样。

var maybeOf = function(f) {
  return function(m) {
    if (m instanceof None) {
      return m;
    } else if (m instanceof Just) {
      return just(f(m.x));
    } else {
      throw new TypeError("Error: Just or None expected, " +
        m.toString() + " given.");
    }
  }
}

那么Maybe是单子,maybe是函子,maybeOf返回一个已经分配了态射的函子。

在我们继续往下进行之前,我们还得做点事情。我们需要给Maybe这个单子对象添加一个方法让它用起来更直观。

Maybe.prototype.orElse = function(y) {
  if (this instanceof Just) {
    return this.x;
  } else {
    return y;
  }
}

在现在这样的形式里,maybe可以直接被使用。

maybe(just(123)).x; // Returns 123
maybeOf(plusplus)(just(123)).x; // Returns 123
maybeOf(plusplus)(none()).orElse('none'); // returns 'none'

所有东西都返回一个方法然后再被调用,这太复杂了,简直是在自找麻烦。我们可以通过我们的curry()函数让它看起来稍微明白一点:

maybePlusPlus = maybeOf.curry()(plusplus);
maybePlusPlus(just(123)).x; // returns 123
maybePlusPlus(none()).orElse('none'); // returns none

不过当直接调用none()和just()这些脏业务被抽象起来的时候,maybe真正的力量就变得明显了。我们用一个User对象的例子来试一下,这里username将使用maybe。

var User = function() {
  this.username = none(); // 初始设置为`none`
};
User.prototype.setUsername = function(name) {
  this.username = just(str(name)); // 这里用一个just
};
User.prototype.getUsernameMaybe = function() {
  var usernameMaybe = maybeOf.curry()(str);
  return usernameMaybe(this.username).orElse('anonymous');
};
var user = new User();
user.getUsernameMaybe(); // Returns 'anonymous'
user.setUsername('Laura');
user.getUsernameMaybe(); // Returns 'Laura'

现在我们有了强大且安全的方法来定义默认值。记住这个User对象,因为在后面的章节里要用到它。

Promise

承诺(Promise)的本质就是它不受形式变化的影响
- Frank Underwood, 纸牌屋

在函数式编程中,我们经常使用管道和数据流:也就是函数链,每个函数产生的数据类型被下一个函数消费。然而,有很多这些函数是异步的:文件、事件、Ajax等等。如果不用持续传递的风格和深层嵌套的回调,我们该如何修改这些函数的返回类型来说明结果?把它们封装到promise里。

promise像是回调的函数式等价物。很明显,回调不是那么函数式,如果不止一个函数要改变同样的数据,就会出现竞争条件和bug。promise解决了这个问题。

不用promise的代码是这样:

fs.readFile("file.json", function(err, val) {
  if (err) {
    console.error("unable to read file");
  } else {
    try {
      val = JSON.parse(val);
      console.log(val.success);
    } catch (e) {
      console.error("invalid json in file");
    }
  }
});

用promise应该把代码编程这样:

fs.readFileAsync("file.json").then(JSON.parse)
  .then(function(val) {
    console.log(val.success);
  })
  .catch(SyntaxError, function(e) {
    console.error("invalid json in file");
  })
  .catch(function(e) {
    console.error("unable to read file")
  });

前面的代码来自于bluebird的README,bluebird是一个对Promises/A+的全面实现,并有非常好的性能。Promises/A+是一个JavaScript中promise的实现规范。这里只给出JavaScript社区当前的成果,把实现留给Promises/A+团队吧,因为它比maybe复杂得多。

不过这里给出部分实现:

// Promise单子
var Promise = require('bluebird');
// promise函子
var promise = function(fn, receiver) {
  return function() {
    var slice = Array.prototype.slice,
      args = slice.call(arguments, 0, fn.length - 1),
      promise = new Promise();
    args.push(function() {
      var results = slice.call(arguments),
        error = results.shift();
      if (error) promise.reject(error);
      else promise.resolve.apply(promise, results);
    });
    fn.apply(receiver, args);
    return promise;
  };
};

现在我们可以利用函子promise()把需要传入回调的函数改为返回promise的函数。

var files = ['a.json', 'b.json', 'c.json'];
readFileAsync = promise(fs.readFile);
var data = files
  .map(function(f) {
    readFileAsync(f).then(JSON.parse)
  })
  .reduce(function(a, b) {
    return $.extend({}, a, b)
  });

lens

程序员真正喜欢单子的另一个原因是它们使编写库非常简单。为了一探究竟,我们来给User对象扩展出更多的函数,这些函数用于设置和获取值,不过我们不用getter和setter,而使用lens。

lens是头等getter和setter,它让我们不仅可以设置和获取值,还可以直接运行函数。不过它不会改变数据,而是克隆一份数据,函数对克隆出来的数据进行修改后返回。它强制数据不可变,这对很多库所需要的安全性和一致性都很有好处。它有益于写出优雅的代码,无论什么样的应用,只要拷贝数组带来的增加量造成的性能冲击不是什么大问题。

在我们写lens()函数前,先来看看它是如何工作的:

var first = lens(
  function(a) { return arr(a)[0]; }, // get
  function(a, b) { return [b].concat(arr(a).slice(1)); } // set
);
first([1, 2, 3]); // 输出 1
first.set([1, 2, 3], 5); // 输出 [5, 2, 3]
function tenTimes(x) { return x * 10 }
first.modify(tenTimes, [1, 2, 3]); // 输出 [10,2,3]

下面展示了lens()是函数如何工作的。它返回了一个定义了set、get和mod的函数。lens()函数本身是个函子。

var lens = fuction(get, set) {
  var f = function (a) {return get(a)};
  f.get = function (a) {return get(a)};
  f.set = set;
  f.mod = function (f, a) {return set(a, f(get(a)))};
  return f;
};

来试个例子,我们要扩展前面例子中的User对象。

// userName :: User -> str
var userName = lens(
  function(u) {
    return u.getUsernameMaybe()
  }, // get
  function(u, v) { // set
    u.setUsername(v);
    return u.getUsernameMaybe();
  }
);
var bob = new User();
bob.setUsername('Bob');
userName.get(bob); // 返回'Bob'
userName.set(bob, 'Bobby'); //return 'Bobby'
userName.get(bob); // 返回'Bobby'
userName.mod(strToUpper, bob); // 返回'BOBBY'
strToUpper.compose(userName.set)(bob, 'robert'); // 返回'ROBERT'
userName.get(bob); // 返回'robert'

jQuery是一个单子

如果你觉得所有这些关于范畴、函子和单子的抽象的玩意儿并没有在真实世界的应用,那你再想想。jQuery,这个最流行的JavaScript库,它提供了操作HTML的增强接口,实际上,它是一个单子化的库。

jQuery对象是一个单子,它的方法是函子。实际上它们是一种特殊类型的函子,叫做endofunctor。endofunctor是返回与输入相同范畴的函子,也就是F :: X -> X。每个jQuery方法都取一个jQuery对象并返回一个jQuery对象,这就使得方法可以链式调用。它们的类型签名是jFunc :: jquery-obj -> jquery-obj。

$('li').add('p.me-too').css('color', 'red').attr({id:'foo'});

这也用于jQuery的插件框架。如果一个插件以jQuery对象为输入,并返回一个jQuery对象为输出,那它就可以被添加到方法链中。

来看下jQuery是如何实现这个的。

单子是函子“伸手进去”取数据的容器。通过这种方式,数据就可以被保护起来并由库来控制。jQuery通过一些方法提供了访问里面数据(一系列封装起来的HTML元素)的方式。

jQuery自身是写成一个匿名函数调用的结果。

var jQuery = (function() {
  var j = function(selector, context) {
    var jq - obj = new j.fn.init(selector, context);
    return jq - obj;
  };
  j.fn = j.prototype = {
    init: function(selector, context) {
      if (!selector) {
        return this;
      }
    }
  };
  j.fn.init.prototype = j.fn;
  return j;
})();

在这个高度简化的jQuery版本里,它返回了一个定义了j对象的函数。j函数实际上是一个增强了的init构造器。

var $ = jQuery(); // 从这个函数得到了返回值并赋值给`$`
var x = $('#select-me'); // 返回了jQuery对象

与函子从容器中取用值的方式相同,jQuery把HTML元素封装了起来,并提供了访问他们的方法,而没有直接去修改HTML元素。

尽管jQuery不经常宣扬它,但jQuery有自己的map方法用于从封装中取出HTML元素对象。就像fmap()方法一样,这些元素被取出,对它们做一些事情,然后放回到容器中。这就是许多jQuery命令后期的使用方式。

$('li').map(function(index, element) {
  // do something to the element
  return element
});

另一个用于操作HTML元素的库Prototype就不是这样工作的。它通过一些辅助方式直接修改HTML元素。因此它在JavaScript社区里就没有那样的地位。

posted @ 2016-05-11 23:39  tolg  阅读(2099)  评论(4编辑  收藏  举报