摘要:但是,如果我們使用過多的函數式編程的抽象概念,我們的函數式編程也會非常難以理解。相比于不太合理的純函數式編程,我們的代碼更加可讀理解和修改,這也是我們重構代碼的目的。
本文是篇譯文,原文鏈接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 $("", { 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
創建", {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)=> $("", { 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) => $("", {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) => $("", {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
讓我們來通過實踐看一下函數式編程能如何改善下面的代碼
let items = ["a", "b", "c"]; let upperCaseItems = () => { let arr = []; for (let i=0, ii= items.length; i共享狀態來簡化函數
這看起來很明顯且微不足道,但是我還是讓函數訪問和修改了外部的狀態,這讓函數難以測試且容易出錯。
//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) => $("", {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(""); 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 ", () => { 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 ", (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 .flickr ? returns a Promise that resolves with a NodeList of 3 passing (67ms)到這里,我們已經成功的測試了我們的模塊以及組成它的函數,學習到了純函數以及如何使用函數組合。我們知道了純函數與不純函數的區別,知道純函數更可讀,由小函數組成,更容易測試。相比于不太合理的純函數式編程,我們的代碼更加可讀、理解和修改,這也是我們重構代碼的目的。
LinksProfessor Frisby’s Mostly Adequate Guide to Functional Programming – @drboolean-這是一本很優秀的介紹函數式編程的書,本文的很多內容和例子出自這本書
Eloquent Javascript – Functional Programming @marijnjh-介紹編程的好書,同樣有一章介紹函數式編程的內容很棒
Underscore-深入的挖掘像Underscore,lodash,Ramda這樣的工具庫是成為成熟開發者的重要一步。理解如何使用這些函數將極大降低你代碼的長度,讓你的程序更加聲明式的。
以上就是本文的全部!非常感謝閱讀,我希望這篇文章很好的向你介紹了函數式編程,重構以及測試你的JavaScript。由于目前特別火熱的庫如React,Redux,Elm,Cycle和ReactiveX都在鼓勵和使用這種模式,所以這個時候寫這樣一篇有趣的范例也算是推波助流吧。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/80897.html
摘要:函數式編程,一看這個詞,簡直就是學院派的典范。所以這期周刊,我們就重點引入的函數式編程,淺入淺出,一窺函數式編程的思想,可能讓你對編程語言的理解更加融會貫通一些。但從根本上來說,函數式編程就是關于如使用通用的可復用函數進行組合編程。 showImg(https://segmentfault.com/img/bVGQuc); 函數式編程(Functional Programming),一...
摘要:函數式編程的哲學就是假定副作用是造成不正當行為的主要原因。函數組合面向對象通常被比喻為名詞,而函數式編程是動詞。尾遞歸優化函數式編程語言中因為不可變數據結構的原因,沒辦法實現循環。 零、前言 說到函數式編程,想必各位或多或少都有所耳聞,然而對于函數式的內涵和本質可能又有些說不清楚。 所以本文希望針對工程師,從應用(而非學術)的角度將函數式編程相關思想和實踐(以 JavaScript 為...
閱讀 2183·2021-11-19 09:40
閱讀 1919·2021-11-08 13:24
閱讀 2453·2021-10-18 13:24
閱讀 2858·2021-10-11 10:57
閱讀 3578·2021-09-22 15:42
閱讀 1114·2019-08-29 17:11
閱讀 2528·2019-08-29 16:11
閱讀 2421·2019-08-29 11:11