摘要:但是,如果我们使用过多的函数式编程的抽象概念,我们的函数式编程也会非常难以理解。相比于不太合理的纯函数式编程,我们的代码更加可读理解和修改,这也是我们重构代码的目的。
本文是篇译文,原文链接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都在鼓励和使用这种模式,所以这个时候写这样一篇有趣的范例也算是推波助流吧。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/80897.html
摘要:函数式编程,一看这个词,简直就是学院派的典范。所以这期周刊,我们就重点引入的函数式编程,浅入浅出,一窥函数式编程的思想,可能让你对编程语言的理解更加融会贯通一些。但从根本上来说,函数式编程就是关于如使用通用的可复用函数进行组合编程。 showImg(https://segmentfault.com/img/bVGQuc); 函数式编程(Functional Programming),一...
摘要:函数式编程的哲学就是假定副作用是造成不正当行为的主要原因。函数组合面向对象通常被比喻为名词,而函数式编程是动词。尾递归优化函数式编程语言中因为不可变数据结构的原因,没办法实现循环。 零、前言 说到函数式编程,想必各位或多或少都有所耳闻,然而对于函数式的内涵和本质可能又有些说不清楚。 所以本文希望针对工程师,从应用(而非学术)的角度将函数式编程相关思想和实践(以 JavaScript 为...
阅读 2182·2021-11-19 09:40
阅读 1917·2021-11-08 13:24
阅读 2452·2021-10-18 13:24
阅读 2857·2021-10-11 10:57
阅读 3577·2021-09-22 15:42
阅读 1114·2019-08-29 17:11
阅读 2527·2019-08-29 16:11
阅读 2420·2019-08-29 11:11