一个函数式的 flickr

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

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.1.11/require.min.js"></script>
  5. <script src="flickr.js"></script>
  6. </head>
  7. <body></body>
  8. </html>

flickr.js 如下:

  1. requirejs.config({
  2. paths: {
  3. ramda: 'https://cdnjs.cloudflare.com/ajax/libs/ramda/0.13.0/ramda.min',
  4. jquery: 'https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min'
  5. }
  6. });
  7. require([
  8. 'ramda',
  9. 'jquery'
  10. ],
  11. function (_, $) {
  12. var trace = _.curry(function(tag, x) {
  13. console.log(tag, x);
  14. return x;
  15. });
  16. // app goes here
  17. });

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

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

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

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

  1. var Impure = {
  2. getJSON: _.curry(function(callback, url) {
  3. $.getJSON(url, callback);
  4. }),
  5. setHtml: _.curry(function(sel, html) {
  6. $(sel).html(html);
  7. })
  8. };

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

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

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

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

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

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

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

一个函数式的 flickr - 图1

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

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

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

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

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

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

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

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

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

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

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

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

任务完成!

一个函数式的 flickr - 图2

下面是完整代码:

  1. requirejs.config({
  2. paths: {
  3. ramda: 'https://cdnjs.cloudflare.com/ajax/libs/ramda/0.13.0/ramda.min',
  4. jquery: 'https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min'
  5. }
  6. });
  7. require([
  8. 'ramda',
  9. 'jquery'
  10. ],
  11. function (_, $) {
  12. ////////////////////////////////////////////
  13. // Utils
  14. var Impure = {
  15. getJSON: _.curry(function(callback, url) {
  16. $.getJSON(url, callback);
  17. }),
  18. setHtml: _.curry(function(sel, html) {
  19. $(sel).html(html);
  20. })
  21. };
  22. var img = function (url) {
  23. return $('<img />', { src: url });
  24. };
  25. var trace = _.curry(function(tag, x) {
  26. console.log(tag, x);
  27. return x;
  28. });
  29. ////////////////////////////////////////////
  30. var url = function (t) {
  31. return 'https://api.flickr.com/services/feeds/photos_public.gne?tags=' + t + '&format=json&jsoncallback=?';
  32. };
  33. var mediaUrl = _.compose(_.prop('m'), _.prop('media'));
  34. var srcs = _.compose(_.map(mediaUrl), _.prop('items'));
  35. var images = _.compose(_.map(img), srcs);
  36. var renderImages = _.compose(Impure.setHtml("body"), images);
  37. var app = _.compose(Impure.getJSON(renderImages), url);
  38. app("cats");
  39. });

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

有原则的重构

上面的代码是有优化空间的——我们获取 url map 了一次,把这些 url 变为 img 标签又 map 了一次。关于 map 和组合是有定律的:

  1. // map 的组合律
  2. var law = compose(map(f), map(g)) == map(compose(f, g));

我们可以利用这个定律优化代码,进行一次有原则的重构。

  1. // 原有代码
  2. var mediaUrl = _.compose(_.prop('m'), _.prop('media'));
  3. var srcs = _.compose(_.map(mediaUrl), _.prop('items'));
  4. var images = _.compose(_.map(img), srcs);

感谢等式推导(equational reasoning)及纯函数的特性,我们可以内联调用 srcsimages,也就是把 map 调用排列起来。

  1. var mediaUrl = _.compose(_.prop('m'), _.prop('media'));
  2. var images = _.compose(_.map(img), _.map(mediaUrl), _.prop('items'));

map 排成一列之后就可以应用组合律了。

  1. var mediaUrl = _.compose(_.prop('m'), _.prop('media'));
  2. var images = _.compose(_.map(_.compose(img, mediaUrl)), _.prop('items'));

现在只需要循环一次就可以把每一个对象都转为 img 标签了。我们把 map 调用的 compose 取出来放到外面,提高一下可读性。

  1. var mediaUrl = _.compose(_.prop('m'), _.prop('media'));
  2. var mediaToImg = _.compose(img, mediaUrl);
  3. var images = _.compose(_.map(mediaToImg), _.prop('items'));

总结

我们已经见识到如何在一个小而不失真实的应用中运用新技能了,也已经使用过函数式这个“数学框架”来推导和重构代码了。但是异常处理以及代码分支呢?如何让整个应用都是函数式的,而不仅仅是把破坏性的函数放到命名空间下?如何让应用更安全更富有表现力?这些都是本书第 2 部分将要解决的问题。