Functional programming can reduce the complexity of the program: a function looks like a mathematical formula. Learning functional programming can help you write simpler code with fewer bugs.
Pure function
A pure function can be understood as a function that must have the same output with the same input, without any observable side effects
//pure function add(a + b) { return a + b; }
The above is a pure function, it does not depend on or change any variables other than the function state, always returns the same output for the same input.
//impure var minimum = 21; var checkAge = function(age) { return age >= minimum; // 如果minimum改变,函数结果也会改变 }
This function is not a pure function because it relies on external mutable state
If we move the variable inside the function, then it becomes a pure function, so that we can ensure that the function can be compared correctly every time age.
var checkAge = function(age) { var minimum = 21; return age >= minimum; };
Pure functions have no side effects, some things you need to remember is that it does not:
Access system state outside the function
Modify objects passed as parameters
Initiate http requests
Preserve user input
Querying the DOM
Controlled mutation
You need to pay attention to some mutation methods that will change arrays and objects. For example, you need to know the difference between splice and slice.
//impure, splice 改变了原数组 var firstThree = function(arr) { return arr.splice(0,3); } //pure, slice 返回了一个新数组 var firstThree = function(arr) { return arr.slice(0,3); }
If we avoid using mutator methods on the objects passed into the function, our programs will be easier to understand, and we can reasonably expect that our functions will not change anything outside the function.
let items = ['a', 'b', 'c']; let newItems = pure(items); //对于纯函数items始终应该是['a', 'b', 'c']
Advantages of pure functions
Compared to impure functions, pure functions have the following advantages:
Easier to test, because their only responsibility is to calculate output based on input
The results can be cached, because the same Input will always get the same output
Self-documenting because the dependencies of the function are clear
Easier to call because you don’t have to worry about the side effects of the function
Because the results of pure functions can be cached, we can remember Keep them in place so that complex and expensive operations only need to be performed once when called. For example, caching the results of a large query index can greatly improve program performance.
Unreasonable pure function programming
Using pure functions can greatly reduce the complexity of the program. However, if we use too many abstract concepts of functional programming, our functional programming will also be very difficult to understand.
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");
Take a minute to understand the above code.
Unless you are exposed to these concepts of functional programming (currying, composition and props), it will be difficult to understand the above code. Compared with the purely functional approach, the following code is easier to understand and modify, it describes the program more clearly and requires less code.
The parameter of the app function is a tag string
Get JSON data from Flickr
Extract urls from the returned data
Create a1f02c36ba31691bcfe87b2722de723b node array
Insert them into the document
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");
Or you can use fetch and Promise to better perform asynchronous operations.
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 requests and DOM operations are not pure, but we can form the remaining operations into pure functions and convert the returned JSON data into an array of image nodes.
let responseToImages = (resp) => { let urls = resp.items.map((item) => item.media.m) let images = urls.map((url) => $('<img />', {src:url})) return images }
Our function does 2 things:
Convert the returned data into urls
Convert urls into image nodes
The functional approach is to split the above 2 tasks, and then use compose to combine a function The result is passed as a parameter to another parameter.
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 returns a combination of functions, each function will use the result of the latter function as its own input parameter
What compose does here is to pass the result of urls into the images function
let responseToImages = (data) => { return images(urls(data)) }
By changing the code into Making them pure functions gives us the opportunity to reuse them in the future, and they are easier to test and self-documenting. The bad thing is that when we overuse these functional abstractions (like in the first example), it complicates things, which is not what we want. The most important thing to ask yourself when we refactor code is:
Does this make the code easier to read and understand?
基本功能函数
我并不是要诋毁函数式编程。每个程序员都应该齐心协力去学习基础函数,这些函数让你在编程过程中使用一些抽象出的一般模式,写出更加简洁明了的代码,或者像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)
到这里,我们已经成功的测试了我们的模块以及组成它的函数,学习到了纯函数以及如何使用函数组合。我们知道了纯函数与不纯函数的区别,知道纯函数更可读,由小函数组成,更容易测试。相比于不太合理的纯函数式编程,我们的代码更加可读、理解和修改,这也是我们重构代码的目的。