関数プログラミングはプログラムの複雑さを軽減できます。関数は数式のように見えます。関数型プログラミングを学習すると、バグの少ない、より単純なコードを作成できるようになります。
純粋関数
純粋関数は、目に見える副作用がなく、同じ入力で同じ出力を持たなければならない関数として理解できます
//pure function add(a + b) { return a + b; }
上記は純粋関数であり、変数に依存したり変更したりしませんState 関数以外の関数は、同じ入力に対して常に同じ出力を返します。
//impure var minimum = 21; var checkAge = function(age) { return age >= minimum; // 如果minimum改变,函数结果也会改变 }
この関数は外部の可変状態に依存しているため、純粋関数ではありません
関数内で変数を移動すると、純粋関数になり、関数が経過するたびに正しく比較できるようになります。
var checkAge = function(age) { var minimum = 21; return age >= minimum; };
純粋な関数には副作用はありませんが、副作用がないことを覚えておく必要があります:
関数の外部でシステム状態にアクセスする
パラメータとして渡されたオブジェクトを変更する
http リクエストを開始する
ユーザー入力を保持する
クエリを実行するDOM
制御された突然変異
配列とオブジェクトを変更するいくつかの突然変異メソッドに注意する必要があります。たとえば、スプライスとスライスの違いを知る必要があります。
//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']
純粋関数の利点
不純関数と比較して、純粋関数には次の利点があります:
入力に基づいて出力を計算することだけを行うため、テストが簡単です
同じであるため、結果をキャッシュできます入力は常に同じ出力を取得します
関数の依存関係が明確であるため自己文書化されています
関数の副作用を気にする必要がないため呼び出しが簡単です
純粋な関数の結果は次のとおりであるためキャッシュされているため、それらを適切な場所に保持しておくと、複雑でコストのかかる操作が呼び出されたときに 1 回だけ実行するだけで済みます。たとえば、大規模なクエリ インデックスの結果をキャッシュすると、プログラムのパフォーマンスが大幅に向上します。
不合理な純粋関数プログラミング
純粋関数を使用すると、プログラムの複雑さを大幅に軽減できます。ただし、関数型プログラミングの抽象的な概念を多用しすぎると、関数型プログラミングも非常に理解しにくくなります。
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");
上記のコードを理解するために少し時間を取ってください。
関数型プログラミングのこれらの概念 (カリー化、合成、小道具) に慣れていない限り、上記のコードを理解するのは難しいでしょう。純粋に関数型のアプローチと比較して、次のコードは理解と変更が容易で、プログラムをより明確に記述し、必要なコードの量が少なくなります。
アプリ関数のパラメータはタグ文字列です
FlickrからJSONデータを取得します
返されたデータからURLを抽出します
ノード配列を作成します
それらをドキュメントに挿入します
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 つのことを行います:
返されたデータを URL に変換します
URL を画像ノードに変換します
関数的なアプローチは、上記の 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 が行うことは、URL の結果を画像関数に渡すことです
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)
到这里,我们已经成功的测试了我们的模块以及组成它的函数,学习到了纯函数以及如何使用函数组合。我们知道了纯函数与不纯函数的区别,知道纯函数更可读,由小函数组成,更容易测试。相比于不太合理的纯函数式编程,我们的代码更加可读、理解和修改,这也是我们重构代码的目的。