合理的使用纯函数式编程

本文是篇译文,原文连接An Introduction to Reasonably Pure Functional Programming,不当之处还请指正。javascript

一个好的程序员应该有能力掌控你写的代码,可以以最简单的方法使你的代码正确而且可读。做为一名优秀的程序员,你会编写尽可能短小的函数,使代码更好的被复用;你会编写测试代码,使本身有足够的信心相信代码会按本来的意图正确运行。没有人喜欢解bug,因此一名优秀的程序员也要会避免一些错误,这些要靠经验得到,也能够遵循一些最佳实践,好比Douglas Crockford 最著名的JavaScript:The good partshtml

函数式编程可以下降程序的复杂程度:函数看起来就像是一个数学公式。学习函数编程可以帮助你编写简单而且更少bug的代码。java

纯函数

纯函数能够理解为一种 相同的输入一定有相同的输出的函数,没有任何能够观察到反作用node

//pure
function add(a + b) {
  return a + b;
}

上面是一个纯函数,它不依赖也不改变任何函数之外的变量状态,对于相同的输入总能返回相同的输出。react

//impure
var minimum = 21;
var checkAge = function(age) {
  return age >= minimum; // 若是minimum改变,函数结果也会改变
}

这个函数不是纯函数,由于它依赖外部可变的状态jquery

若是咱们将变量移到函数内部,那么它就变成了纯函数,这样咱们就可以保证函数每次都能正确的比较年龄。git

var checkAge = function(age) {
  var minimum = 21;
  return age >= minimum;
};

纯函数没有反作用,一些你要记住的是,它不会:程序员

  • 访问函数之外的系统状态es6

  • 修改以参数形式传递过来的对象github

  • 发起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");

或者可使用fetchPromise来更好的进行异步操做。

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提供了一些好用的函数如describeit来拆分测试和钩子(例如before和after这种用来组装和拆分任务的钩子)。assert是用来进行相等测试的断言库,assertassert.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函数fetchPromise。为了测试他们,咱们使用jsdom来模拟DOM对象windowdocument,使用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

以上就是本文的所有!很是感谢阅读,我但愿这篇文章很好的向你介绍了函数式编程,重构以及测试你的JavaScript。因为目前特别火热的库如React,Redux,Elm,CycleReactiveX都在鼓励和使用这种模式,因此这个时候写这样一篇有趣的范例也算是推波助流吧。

相关文章
相关标签/搜索