一个函数式的 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 提供了 compose
、curry
等很多函数。模块加载我们选择的是 requirejs,我以前用过 requirejs,虽然它有些重,但为了保持一致性,本书将一直使用它。另外,我也把 trace
函数写好了,便于 debug。
有点跑题了。言归正传,我们的应用将做 4 件事:
- 根据特定搜索关键字构造 url
- 向 flickr 发送 api 请求
- 把返回的 json 转为 html 图片
- 把图片放到屏幕上
注意到没?上面提到了两个不纯的动作,即从 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");
});
看看,多么美妙的声明式规范啊,只说做什么,不说怎么做。现在我们可以把每一行代码都视作一个等式,变量名所代表的属性就是等式的含义。我们可以利用这些属性去推导分析和重构这个应用。
有原则的重构
上面的代码是有优化空间的——我们获取 url map 了一次,把这些 url 变为 img 标签又 map 了一次。关于 map 和组合是有定律的:
// map 的组合律
var law = compose(map(f), map(g)) == map(compose(f, g));
我们可以利用这个定律优化代码,进行一次有原则的重构。
// 原有代码
var mediaUrl = _.compose(_.prop('m'), _.prop('media'));
var srcs = _.compose(_.map(mediaUrl), _.prop('items'));
var images = _.compose(_.map(img), srcs);
感谢等式推导(equational reasoning)及纯函数的特性,我们可以内联调用 srcs
和 images
,也就是把 map 调用排列起来。
var mediaUrl = _.compose(_.prop('m'), _.prop('media'));
var images = _.compose(_.map(img), _.map(mediaUrl), _.prop('items'));
把 map
排成一列之后就可以应用组合律了。
var mediaUrl = _.compose(_.prop('m'), _.prop('media'));
var images = _.compose(_.map(_.compose(img, mediaUrl)), _.prop('items'));
现在只需要循环一次就可以把每一个对象都转为 img 标签了。我们把 map 调用的 compose 取出来放到外面,提高一下可读性。
var mediaUrl = _.compose(_.prop('m'), _.prop('media'));
var mediaToImg = _.compose(img, mediaUrl);
var images = _.compose(_.map(mediaToImg), _.prop('items'));
总结
我们已经见识到如何在一个小而不失真实的应用中运用新技能了,也已经使用过函数式这个“数学框架”来推导和重构代码了。但是异常处理以及代码分支呢?如何让整个应用都是函数式的,而不仅仅是把破坏性的函数放到命名空间下?如何让应用更安全更富有表现力?这些都是本书第 2 部分将要解决的问题。