合理的使用纯函数式编程
本文是篇譯文,原文鏈接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>節點數組
將他們插入文檔
或者可以使用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.jsMocha提供了一些好用的函數如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 numberFlickr._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都在鼓勵和使用這種模式,所以這個時候寫這樣一篇有趣的范例也算是推波助流吧。
總結
以上是生活随笔為你收集整理的合理的使用纯函数式编程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 金蝶记账王无法打开智能卡注册页面怎么办
- 下一篇: 阿里云云市场双11战报:30分钟破100