一个函数式的 flickr

现在我们以一种声明式的、可组合的方式创建一个示例应用。暂时我们还是会作点小弊,使用副作用;但我们会把副作用的程度降到最低,让它们与纯函数代码分离开来。这个示例应用是一个浏览器 widget,功能是从 flickr 获取图片并在页面上展示。我们从写 HTML 开始:

<!DOCTYPE html>
<html>
  <head>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.1.11/require.min.js"></script>
    <script src="flickr.js"></script>
  </head>
  <body></body>
</html>

flickr.js 如下:

requirejs.config({
  paths: {
    ramda: 'https://cdnjs.cloudflare.com/ajax/libs/ramda/0.13.0/ramda.min',
    jquery: 'https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min'
  }
});

require([
    'ramda',
    'jquery'
  ],
  function (_, $) {
    var trace = _.curry(function(tag, x) {
      console.log(tag, x);
      return x;
    });
    // app goes here
  });

这里我们使用了 ramda ,没有用 lodash 或者其他类库。ramda 提供了 composecurry 等很多函数。模块加载我们选择的是 requirejs,我以前用过 requirejs,虽然它有些重,但为了保持一致性,本书将一直使用它。另外,我也把 trace 函数写好了,便于 debug。

有点跑题了。言归正传,我们的应用将做 4 件事:

  1. 根据特定搜索关键字构造 url
  2. 向 flickr 发送 API 请求
  3. 把返回的 JSON 转为 HTML 图片
  4. 把图片放到屏幕上

注意到没?上面提到了两个不纯的动作,即从 flickr 的 API 获取数据和在屏幕上放置图片这两件事。我们先来定义这两个动作,这样就能隔离它们了。

var Impure = {
  getJSON: _.curry(function(callback, url) {
    $.getJSON(url, callback);
  }),

  setHtml: _.curry(function(sel, html) {
    $(sel).html(html);
  })
};

这里只是简单地包装了一下 jQuery 的 getJSON 方法,把它变为一个 curry 函数,还有就是把参数位置也调换了下。这些方法都在 Impure 命名空间下,这样我们就知道它们都是危险函数。在后面的例子中,我们会把这两个函数变纯。

下一步是构造 url 传给 Impure.getJSON 函数。

var url = function (term) {
  return 'https://api.flickr.com/services/feeds/photos_public.gne?tags=' + term + '&format=json&jsoncallback=?';
};

借助 monoid 或 combinator (后面会讲到这些概念),我们可以使用一些奇技淫巧来让 url 函数变为 pointfree 函数。但是为了可读性,我们还是选择以普通的非 pointfree 的方式拼接字符串。

让我们写一个 app 函数发送请求并把内容放置到屏幕上。

var app = _.compose(Impure.getJSON(trace("response")), url);
app("cats");

这会调用 url 函数,然后把字符串传给 getJSON 函数。getJSON 已经局部应用了 trace,加载这个应用将会把请求的响应显示在 console 里。

我们想要从这个 JSON 里构造图片,看起来 src 都在 items 数组中的每个 media 对象的 m 属性上。

不管怎样,我们可以使用 ramda 的一个通用 getter 函数 _.prop() 来获取这些嵌套的属性。不过为了让你明白这个函数做了什么事情,我们自己实现一个 prop 看看:

var prop = _.curry(function(property, object){
  return object[property];
});

实际上这有点傻,仅仅是用 [] 来获取一个对象的属性而已。让我们利用这个函数获取图片的 src。

var mediaUrl = _.compose(_.prop('m'), _.prop('media'));
var srcs = _.compose(_.map(mediaUrl), _.prop('items'));

一旦得到了 items,就必须使用 map 来分解每一个 url;这样就得到了一个包含所有 src 的数组。把它和 app 联结起来,打印结果看看。

var renderImages = _.compose(Impure.setHtml("body"), srcs);
var app = _.compose(Impure.getJSON(renderImages), url);

这里所做的只不过是新建了一个组合,这个组合会调用 srcs 函数,并把返回结果设置为 body 的 HTML。我们也把 trace 替换为了 renderImages,因为已经有了除原始 JSON 以外的数据。这将会粗暴地把所有的 src 直接显示在屏幕上。

最后一步是把这些 src 变为真正的图片。对大型点的应用来说,是应该使用类似 Handlebars 或者 React 这样的 template/dom 库来做这件事的。但我们这个应用太小了,只需要一个 img 标签,所以用 jQuery 就好了。

var img = function (url) {
  return $('<img />', { src: url });
};

jQuery 的 html() 方法接受标签数组为参数,所以我们只须把 src 转换为 img 标签然后传给 setHtml 即可。

var images = _.compose(_.map(img), srcs);
var renderImages = _.compose(Impure.setHtml("body"), images);
var app = _.compose(Impure.getJSON(renderImages), url);

任务完成!

下面是完整代码:

requirejs.config({
  paths: {
    ramda: 'https://cdnjs.cloudflare.com/ajax/libs/ramda/0.13.0/ramda.min',
    jquery: 'https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min'
  }
});

require([
    'ramda',
    'jquery'
  ],
  function (_, $) {
    ////////////////////////////////////////////
    // Utils

    var Impure = {
      getJSON: _.curry(function(callback, url) {
        $.getJSON(url, callback);
      }),

      setHtml: _.curry(function(sel, html) {
        $(sel).html(html);
      })
    };

    var img = function (url) {
      return $('<img />', { src: url });
    };

    var trace = _.curry(function(tag, x) {
      console.log(tag, x);
      return x;
    });

    ////////////////////////////////////////////

    var url = function (t) {
      return 'https://api.flickr.com/services/feeds/photos_public.gne?tags=' + t + '&format=json&jsoncallback=?';
    };

    var mediaUrl = _.compose(_.prop('m'), _.prop('media'));

    var srcs = _.compose(_.map(mediaUrl), _.prop('items'));

    var images = _.compose(_.map(img), srcs);

    var renderImages = _.compose(Impure.setHtml("body"), images);

    var app = _.compose(Impure.getJSON(renderImages), url);

    app("cats");
  });

看看,多么美妙的声明式规范啊,只说做什么,不说怎么做。现在我们可以把每一行代码都视作一个等式,变量名所代表的属性就是等式的含义。我们可以利用这些属性去推导分析和重构这个应用。