本文是篇译文,原文链接An Introduction to Reasonably Pure Functional Programming,不当之处还请斧正。
一个好的顺序员应当有才能掌控你写的代码,能够以最简朴的要领使你的代码准确而且可读。作为一位优异的顺序员,你会编写只管短小的函数,使代码更好的被复用;你会编写测试代码,使自身有充足的自信心置信代码会按底本的企图准确运转。没有人喜好解bug,所以一位优异的顺序员也要会防止一些毛病,这些要靠履历取得,也能够遵照一些最好实践,比方Douglas Crockford 最著名的JavaScript:The good parts
函数式编程能够下降顺序的庞杂水平:函数看起来就像是一个数学公式。进修函数编程能够协助你编写简朴而且更少bug的代码。
纯函数
纯函数能够明白为一种 雷同的输入一定有雷同的输出的函数,没有任何能够观察到副作用
//pure
function add(a + b) {
return a + b;
}
上面是一个纯函数,它不依靠也不转变任何函数以外的变量状况,关于雷同的输入总能返回雷同的输出。
//impure
var minimum = 21;
var checkAge = function(age) {
return age >= minimum; // 假如minimum转变,函数效果也会转变
}
这个函数不是纯函数,因为它依靠外部可变的状况
假如我们将变量移到函数内部,那末它就变成了纯函数,如许我们就能够保证函数每次都能准确的比较岁数。
var checkAge = function(age) {
var minimum = 21;
return age >= minimum;
};
纯函数没有副作用,一些你要记着的是,它不会:
接见函数以外的体系状况
修正以参数情势通报过来的对象
提议http要求
保存用户输入
查询DOM
控制增变(controlled mutation)
你须要注意一些会转变数组和对象的增变要领,举例来说你要晓得splice和slice之间的差别。
//impure, splice 转变了原数组
var firstThree = function(arr) {
return arr.splice(0,3);
}
//pure, slice 返回了一个新数组
var firstThree = function(arr) {
return arr.slice(0,3);
}
假如我们防止运用传入函数的对象的增变要领,我们的顺序将更轻易明白,我们也有来由希冀我们的函数不会转变任何函数以外的东西。
let items = ['a', 'b', 'c'];
let newItems = pure(items);
//关于纯函数items一直应当是['a', 'b', 'c']
纯函数的长处
比拟于不纯的函数,纯函数有以下长处:
越发轻易被测试,因为它们唯一的职责就是依据输入盘算输出
效果能够被缓存,因为雷同的输入总会取得雷同的输出
自我文档化,因为函数的依靠关联很清楚
更轻易被挪用,因为你不必忧郁函数会有什么副作用
因为纯函数的效果能够被缓存,我们能够记着他们,如许以来庞杂高贵的操纵只须要在被挪用时实行一次。比方,缓存一个大的查询索引的效果能够极大的改良顺序的机能。
不合理的纯函数编程
运用纯函数能够极大的下降顺序的庞杂度。然则,假如我们运用过量的函数式编程的笼统观点,我们的函数式编程也会异常难以明白。
import _ from 'ramda';
import $ from 'jquery';
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 url = function (t) {
return 'http://api.flickr.com/services/feeds/photos_public.gne?tags=' +
t + '&format=json&jsoncallback=?';
};
var mediaUrl = _.compose(_.prop('m'), _.prop('media'));
var mediaToImg = _.compose(img, mediaUrl);
var images = _.compose(_.map(mediaToImg), _.prop('items'));
var renderImages = _.compose(Impure.setHtml("body"), images);
var app = _.compose(Impure.getJSON(renderImages), url);
app("cats");
花一分钟明白上面的代码。
除非你打仗过函数式编程的这些观点(柯里化,组合和prop),不然很难明白上述代码。比拟于纯函数式的要领,下面的代码则越发轻易明白和修正,它越发清楚的形貌顺序而且更少的代码。
app函数的参数是一个标签字符串
从Flickr猎取JSON数据
从返回的数据里抽出urls
建立<img>节点数组
将他们插进去文档
var app = (tags) => {
let url = `http://api.flickr.com/services/feeds/photos_public.gne?tags=${tags}&format=json&jsoncallback=?`;
$.getJSON(url, (data) => {
let urls = data.items.map((item) => item.media.m)
let images = urls.map(url) => $('<img />', {src:url}) );
$(document.body).html(images);
})
}
app("cats");
或许能够运用fetch和Promise来更好的举行异步操纵。
let flickr = (tags)=> {
let url = `http://api.flickr.com/services/feeds/photos_public.gne?tags=${tags}&format=json&jsoncallback=?`
return fetch(url)
.then((resp)=> resp.json())
.then((data)=> {
let urls = data.items.map((item)=> item.media.m )
let images = urls.map((url)=> $('<img />', { src: url }) )
return images
})
}
flickr("cats").then((images)=> {
$(document.body).html(images)
})
Ajax要乞降DOM操纵都不是纯的,然则我们能够将余下的操纵构成纯函数,将返回的JSON数据转换成图片节点数组。
let responseToImages = (resp) => {
let urls = resp.items.map((item) => item.media.m)
let images = urls.map((url) => $('<img />', {src:url}))
return images
}
我们的函数做了2件事变:
将返回的数据转换成urls
将urls转换成图片节点
函数式的要领是将上述2个使命拆开,然后运用compose将一个函数的效果作为参数传给另一个参数。
let urls = (data) => {
return data.items.map((item) => item.media.m)
}
let images = (urls) => {
return urls.map((url) => $('<img />', {src: url}))
}
let responseToImages = _.compose(images, urls)
compose 返回一系列函数的组合,每一个函数都会将后一个函数的效果作为自身的入参
这里compose做的事变,就是将urls的效果传入images函数
let responseToImages = (data) => {
return images(urls(data))
}
经由过程将代码变成纯函数,让我们在今后有时机复用他们,他们越发轻易被测试和自文档化。不好的是当我们过分的运用这些函数笼统(像第一个例子那样), 就会使事变变得庞杂,这不是我们想要的。当我们重构代码的时刻最主要的是要问一下自身:
这是不是让代码越发轻易浏览和明白?
基本功用函数
我并非要毁谤函数式编程。每一个顺序员都应当齐心协力去进修基本函数,这些函数让你在编程过程当中运用一些笼统出的平常形式,写出越发简洁明了的代码,或许像Marijn Haverbeke说的
一个顺序员能够用通例的基本函数武装自身,更主要的是晓得怎样运用它们,要比那些苦思冥想的人高效的多。– Eloquent JavaScript, Marijn Haverbeke
这里列出了一些JavaScript开发者应当控制的基本函数
Arrays
–forEach
–map
–filter
–reduce
Functions
–debounce
–compose
–partial
–curry
Less is More
让我们来经由过程实践看一下函数式编程能怎样改良下面的代码
let items = ['a', 'b', 'c'];
let upperCaseItems = () => {
let arr = [];
for (let i=0, ii= items.length; i<ii; i++) {
let item = items[i];
arr.push(item.toUpperCase());
}
items = arr;
}
同享状况来简化函数
这看起来很明显且眇乎小哉,然则我照样让函数接见和修正了外部的状况,这让函数难以测试且轻易失足。
//pure
let upperCaseItems = (items) => {
let arr = [];
for (let i =0, ii= items.length; i< ii; i++) {
let item = items[i];
arr.push(item.toUpperCase());
}
return arr;
}
运用越发可读的言语笼统forEach来迭代
let upperCaseItems = (items) => {
let arr = [];
items.forEach((item) => {
arr.push(item.toUpperCase());
})
return arr;
}
运用map进一步简化代码
let upperCaseItems = (items) => {
return items.map((item) => item.toUpperCase())
}
进一步简化代码
let upperCase = (item) => item.toUpperCase()
let upperCaseItems = (item) => items.map(upperCase)
删除代码直到它不能事情
我们不须要为这类简朴的使命编写函数,言语自身就供应了充足的笼统来完胜利用
let items = ['a', 'b', 'c']
let upperCaseItems = item.map((item) => item.toUpperCase())
测试
纯函数的一个症结长处是易于测试,所以在这一节我会为我们之前的Flicker模块编写测试。
我们会运用Mocha来运转测试,运用Babel来编译ES6代码。
mkdir test-harness
cd test-harness
npm init -y
npm install mocha babel-register babel-preset-es2015 --save-dev
echo '{ "presets": ["es2015"] }' > .babelrc
mkdir test
touch test/example.js
Mocha供应了一些好用的函数如describe和it来拆分测试和钩子(比方before和after这类用来组装和拆分使命的钩子)。assert是用来举行相称测试的断言库,assert和assert.deepEqual是很有效且值得注意的函数。
让我们来编写第一个测试test/example.js
import assert from 'assert';
describe('Math', () => {
describe('.floor', () => {
it('rounds down to the nearest whole number', () => {
let value = Math.floor(4.24)
assert(value === 4)
})
})
})
翻开package.json文件,将”test”剧本修正以下
mocha --compilers js:babel-register --recursive
然后你就能够在敕令行运转npm test
Math
.floor
✓ rounds down to the nearest whole number
1 passing (32ms)
Note:假如你想让mocha看管转变,而且自动运转测试,能够在上述敕令背面加上-w选项。
mocha --compilers js:babel-register --recursive -w
测试我们的Flicker模块
我们的模块文件是lib/flickr.js
import $ from 'jquery';
import { compose } from 'underscore';
let urls = (data) => {
return data.items.map((item) => item.media.m)
}
let images = (urls) => {
return urls.map((url) => $('<img />', {src: url})[0] )
}
let responseToImages = compose(images, urls)
let flickr = (tags) => {
let url = `http://api.flickr.com/services/feeds/photos_public.gne?tags=${tags}&format=json&jsoncallback=?`
return fetch(url)
.then((response) => reponse.json())
.then(responseToImages)
}
export default {
_responseToImages: responseToImages,
flickr: flickr
}
我们的模块暴露了2个要领:一个公有flickr和一个私有函数_responseToImages,如许就能够自力的测试他们。
我们运用了一组依靠:jquery,underscore和polyfill函数fetch和Promise。为了测试他们,我们运用jsdom来模仿DOM对象window和document,运用sinon包来测试fetch api。
npm install jquery underscore whatwg-fetch es6-promise jsdom sinon --save-dev
touch test/_setup.js
翻开test/_setup.js,运用全局对象来设置jsdom
global.document = require('jsdom').jsdom('<html></html>');
global.window = document.defaultView;
global.$ = require('jquery')(window);
global.fetch = require('whatwg-fetch').fetch;
我们的测试代码在test/flickr.js,我们将为函数的输出设置断言。我们”stub”或许掩盖全局的fetch要领,来阻断和模仿HTTP要求,如许我们就能够在不直接接见Flickr api的情况下运转我们的测试。
import assert from 'assert';
import Flickr from '../lib/flickr';
import sinon from 'sinon';
import { Promise } from 'es6-promise';
import { Response } from 'whatwg-fetch';
let sampleResponse = {
items: [{
media: { m: 'lolcat.jpg' }
}, {
media: {m: 'dancing_pug.gif'}
}]
}
//现实项目中我们会将这个test helper移到一个模块里
let jsonResponse = (obj) => {
let json = JSON.stringify(obj);
var response = new Response(json, {
status: 200,
headers: {'Content-type': 'application/json'}
});
return Promise.resolve(response);
}
describe('Flickr', () => {
describe('._responseToImages', () => {
it("maps response JSON to a NodeList of <img>", () => {
let images = Flickr._responseToImages(sampleResponse);
assert(images.length === 2);
assert(images[0].nodeName === 'IMG');
assert(images[0].src === 'lolcat.jpg');
})
})
describe('.flickr', () => {
//截断fetch 要求,返回一个Promise对象
before(() => {
sinon.stub(global, 'fetch', (url) => {
return jsonResponse(sampleResponse)
})
})
after(() => {
global.fetch.restore();
})
it("returns a Promise that resolve with a NodeList of <img>", (done) => {
Flickr.flickr('cats').then((images) => {
assert(images.length === 2);
assert(images[1].nodeName === 'IMG');
assert(images[1].src === 'dancing_pug.gif');
done();
})
})
})
})
运转npm test,会获得以下效果:
Math
.floor
✓ rounds down to the nearest whole number
Flickr
._responseToImages
✓ maps response JSON to a NodeList of <img>
.flickr
✓ returns a Promise that resolves with a NodeList of <img>
3 passing (67ms)
到这里,我们已胜利的测试了我们的模块以及构成它的函数,进修到了纯函数以及怎样运用函数组合。我们晓得了纯函数与不纯函数的区分,晓得纯函数更可读,由小函数构成,更轻易测试。比拟于不太合理的纯函数式编程,我们的代码越发可读、明白和修正,这也是我们重构代码的目标。
Links
Professor Frisby’s Mostly Adequate Guide to Functional Programming – @drboolean-这是一本很优异的引见函数式编程的书,本文的许多内容和例子出自这本书
Eloquent Javascript – Functional Programming @marijnjh-引见编程的好书,一样有一章引见函数式编程的内容很棒
Underscore-深切的发掘像Underscore,lodash,Ramda如许的东西库是成为成熟开发者的主要一步。明白怎样运用这些函数将极大下降你代码的长度,让你的顺序越发声明式的。
以上就是本文的悉数!异常感谢浏览,我愿望这篇文章很好的向你引见了函数式编程,重构以及测试你的JavaScript。因为现在迥殊炽热的库如React,Redux,Elm,Cycle和ReactiveX都在勉励和运用这类形式,所以这个时刻写如许一篇风趣的类型也算是推波助流吧。