本文是篇译文,原文链接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>节点数组

  • 将他们插入文档

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");

或者可以使用fetchPromise来更好的进行异步操作。

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.js

Mocha提供了一些好用的函数如describeit来拆分测试和钩子(例如before和after这种用来组装和拆分任务的钩子)。assert是用来进行相等测试的断言库,assertassert.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函数fetchPromise。为了测试他们,我们使用jsdom来模拟DOM对象windowdocument,使用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都在鼓励和使用这种模式,所以这个时候写这样一篇有趣的范例也算是推波助流吧。

合理的使用纯函数式编程相关推荐

  1. 每个人都应该懂点函数式编程

    目录 一个问题 函数式编程中的函数 数学与函数式编程 混合式编程风格 一个问题 假设现在我们需要开发一个绘制数学函数平面图像(一元)的工具库,可以提供绘制各种函数图形的功能,比如直线f(x)=ax+b ...

  2. Python 进阶_函数式编程

    目录 目录 函数式编程 Python 函数式编程的特点 高阶函数 匿名函数 lambda 函数式编程相关的内置函数 filter 序列对象过滤器 map reduce 折叠 自定义的排序函数 最后 函 ...

  3. Haskell 函数式编程快速入门【草】

    什么是函数式编程 用常规编程语言中的函数指针.委托和Lambda表达式等概念来帮助理解(其实函数式编程就是Lambda演算延伸而来的编程范式). 函数式编程中函数可以被非常容易的定义和传递. Hask ...

  4. Swift の 函数式编程

    Swift 相比原先的 Objective-C 最重要的优点之一,就是对函数式编程提供了更好的支持. Swift 提供了更多的语法糖和一些新特性来增强函数式编程的能力,本文就在这方面进行一些讨论. S ...

  5. python语言支持函数式编程_python是函数式语言么

    函数式编程:functional,是一种编程范式. 函数式编程的特点:1. 把计算视为函数而非指令 2. 纯函数式编程:不需要变量,没有副作用,测试简单 3. 支持高阶函数,代码简洁 Python支持 ...

  6. Python 函数式编程,Python中内置的高阶函数:map()、reduce()、filter()与sorted(),Python中返回函数

    函数式编程 是一种编程范式,比函数更高层次的抽象. 函数式编程将计算视为函数而非指令. 纯函数式编程:不需要变量,没有副作用,测试简单. 支持高阶函数,代码简洁. Python 支持的函数式编程 不是 ...

  7. 编程语言的发展趋势及未来方向(3):函数式编程

    关于声明式编程的还有一部分重要的内容,那便是函数式编程.函数式编程已经有很长时间的历史了,当年LISP便是个函数式编程语言.除了LISP以外我们还有其他许多函数式编程语言,如APL.Haskell.S ...

  8. 前端基础进阶(七):函数与函数式编程

    纵观JavaScript中所有必须需要掌握的重点知识中,函数是我们在初学的时候最容易忽视的一个知识点.在学习的过程中,可能会有很多人.很多文章告诉你面向对象很重要,原型很重要,可是却很少有人告诉你,面 ...

  9. javascript函数式_如何以及为什么在现代JavaScript中使用函数式编程

    javascript函数式 by PALAKOLLU SRI MANIKANTA 通过PALAKOLLU SRI MANIKANTA In this article, you will get a d ...

最新文章

  1. (完全解决)为什么运行.bat批处理文件但是只执行了.bat文件中的第一句(行)命令
  2. 通过HTTP协议实现多线程下载
  3. 面试整理(1):原生ajax
  4. jQuery 简单案例
  5. 关于maven面试的哪些事儿~
  6. 结对编程,到底是双剑合璧还是脚趾抠地?
  7. [转]Delphi 12种大小写转换的方法
  8. matlab2c使用c++实现matlab函数系列教程-tril函数
  9. 【语音合成】基于matlab线性预测系数和预测误差语音合成【含Matlab源码 564期】
  10. RNN(三) 在SLU中的应用
  11. .Bear勒索病毒如何删除它 .Bear后缀文件如何恢复(Dharma家族)
  12. PHP随堂笔记时区的设置
  13. SAAS云平台搭建札记: (一) 浅论SAAS多租户自助云服务平台的产品、服务和订单
  14. 《大数据: Hive 介绍与安装》
  15. VNCTF2023 WP
  16. 数据结构——链表经典OJ题题解
  17. 开源分享-Java版超级玛丽
  18. DRM 架构简要说明
  19. ECSHOP和SHOPEX快递单号查询顺丰插件V8.6专版
  20. mysql性能优化曹政_看曹政如何减少SQL请求

热门文章

  1. Matlab保存为unv,matlab之图像处理(2)
  2. nebual的图数据结构
  3. latex插入表格_如何将word表格变成LaTeX代码?
  4. 谈判如何在博弈中获得更多_读后感--《谈判--如何在博弈中获得更多》
  5. springmvc数据验证
  6. 2017年6月份学习总结,读书《5个高效工作习惯,让你跟「瞎忙」划清界限》
  7. 基于Delphi API写的UDP通讯类
  8. Maven项目在pom文件中引入lib下的第三方jar包并打包进去
  9. GeoServer怎样修改线性地图的颜色样式
  10. Centos中Redis的下载编译与安装(超详细)