함수형 프로그래밍은 프로그램의 복잡성을 줄일 수 있습니다. 함수는 수학 공식처럼 보입니다. 함수형 프로그래밍을 배우면 버그를 줄이고 더 간단한 코드를 작성하는 데 도움이 됩니다.
순수 함수
순수 함수는 관찰 가능한 부작용 없이 동일한 입력을 갖고 동일한 출력을 가져야 하는 함수로 이해될 수 있습니다
//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; };
순수 함수에는 부작용이 없습니다.
함수 외부의 시스템 상태에 액세스
수정 사항이 전달됩니다. in as 매개변수 개체
http 요청 시작
사용자 입력 유지
Query DOM
제어된 변이
주의가 필요함 일부 mutator 메소드는 배열과 객체를 변경합니다. 예를 들어 스플라이스와 슬라이스의 차이점을 알아야 합니다.
//impure, splice 改变了原数组 var firstThree = function(arr) { return arr.splice(0,3); } //pure, slice 返回了一个新数组 var firstThree = function(arr) { return arr.slice(0,3); }
함수에 전달된 객체에 mutator 메서드를 사용하지 않으면 프로그램을 더 쉽게 이해할 수 있으며 함수가 함수 외부의 어떤 것도 변경하지 않을 것이라고 합리적으로 기대할 수 있습니다.
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");
잠시 시간을 내어 위 코드를 이해해 보세요.
함수형 프로그래밍의 이러한 개념(커링, 합성, 소품)을 접하지 않으면 위 코드를 이해하기 어렵습니다. 순전히 함수형 접근 방식에 비해 다음 코드는 이해하고 수정하기가 더 쉽고, 프로그램을 더 명확하게 설명하며 더 적은 코드가 필요합니다.
앱 함수의 매개변수는 태그 문자열입니다
Flickr에서 JSON 데이터 가져오기
반환된 데이터에서 URL 추출
a1f02c36ba31691bcfe87b2722de723b 노드 배열
문서
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을 이미지 노드로 변환
함수 전통적인 방법은 다음과 같습니다. 위의 두 작업을 분리한 다음 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)
到这里,我们已经成功的测试了我们的模块以及组成它的函数,学习到了纯函数以及如何使用函数组合。我们知道了纯函数与不纯函数的区别,知道纯函数更可读,由小函数组成,更容易测试。相比于不太合理的纯函数式编程,我们的代码更加可读、理解和修改,这也是我们重构代码的目的。