纯粹函数式编程简介

Panayiotis«pvgr»VelisarakosJezen ThomasFlorian Rappl对该文章进行了同行评审。 感谢所有SitePoint的同行评审人员使SitePoint内容达到最佳状态!

在学习编程时,首先会介绍过程编程。 在这里,您可以通过向计算机提供顺序的命令列表来控制计算机。 了解了一些语言基础知识(如变量,赋值,函数和对象)后,您可以将一个程序拼凑起来,该程序可以实现您设定的功能-就像一个绝对的向导。

成为一名更好的程序员的过程就是获得更大的能力来控制您编写的程序,并找到既正确最易读的最简单解决方案。 当您成为一名更好的程序员时,您将编写较小的函数,实现更好的代码重用,为代码编写测试,并且您将确信自己编写的程序将继续按预期运行。 没有人喜欢发现和修复代码中的错误,因此成为一名更好的程序员也意味着避免某些容易出错的事情。 学习避免的东西来自经验,或者听从那些有经验的人的建议,例如Douglas Crockford在JavaScript:The Good Parts中著名地解释过。

函数式编程为我们提供了通过将程序简化为最简单的形式来降低程序复杂性的方法:函数的行为就像纯数学函数一样。 学习函数式编程的原理是对您的技能的极大补充,并且将帮助您编写更少错误的简单程序。

函数式编程的关键概念是纯函数,不变的值,组成和驯服的副作用。

纯函数

纯函数是在给定相同输入的情况下将始终返回相同输出并且不会产生任何可观察到的副作用的函数。

// pure
function add(a, b) {
  return a + b;
}

此功能是函数。 它不依赖或更改函数外部的任何状态,并且对于相同的输入,它将始终返回相同的输出值。

// impure
var minimum = 21;
var checkAge = function(age) {
  return age >= minimum; // if minimum is changed we're cactus
};

此函数不纯,因为它依赖于函数外部的外部可变状态。

如果将这个变量移到函数内部,它将变为纯函数,并且可以确定我们的函数每次都会正确检查年龄。

// pure
var checkAge = function(age) {
  var minimum = 21;
  return age >= minimum;
};

纯函数没有副作用 。 以下是一些重要的注意事项:

  • 在功能之外访问系统状态
  • 突变作为参数传递的对象
  • 进行HTTP呼叫
  • 获取用户输入
  • 查询DOM

受控突变

您需要了解Arrays和Objects上Mutator方法会更改基础对象,例如Array的spliceslice方法之间的区别就是一个例子。

// impure, splice mutates the array
var firstThree = function(arr) {
  return arr.splice(0,3); // arr may never be the same again
};

// pure, slice returns a new array
var firstThree = function(arr) {
  return arr.slice(0,3);
};

如果我们避免更改传递给函数的对象的方法,那么程序就更容易推理了,我们可以合理地期望我们的函数不会将任何事物从我们的内部转移出去。

let items = ['a','b','c'];
let newItems = pure(items);
// I expect items to be ['a','b','c']

纯函数的好处

纯函数比不纯函数具有一些优点:

  • 更容易测试,因为他们的唯一职责是映射输入->输出
  • 结果可缓存,因为相同的输入总是产生相同的输出
  • 自记录功能的显式性
  • 无需担心副作用,因此更易于使用

因为纯函数的结果是可缓存的,所以我们可以记住它们,因此仅在第一次调用函数时才执行昂贵的操作。 例如,记住搜索大索引的结果将大大提高重新运行的性能。

不合理的纯函数式编程

将程序简化为纯函数可以大大降低程序的复杂性。 但是,如果我们将功能抽象推到太远的话,我们的功能程序也可能最终需要Rain Man的帮助才能理解。

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

花一点时间来消化上面的代码。

除非您具有函数式编程的背景知识,否则这些抽象(curry,过度使用compose和prop)以及执行流程确实很难遵循。 与上面的纯功能方法相比,下面的代码更易于理解和修改,并且比上面的纯功能方法更清楚地描述了程序,并且代码更少。

  • app函数带有一串标签
  • Flickr获取JSON
  • 将网址拉出响应
  • 建立一个<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类的抽象的替代API可以帮助我们进一步阐明异步操作的含义。

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)
})

注意: fetchPromise是即将到来的标准,因此它们需要今天使用polyfills。

Ajax请求和DOM操作永远不会是纯函数,但我们可以从其余函数中提取出一个纯函数,将响应JSON映射到图像数组–现在,我们来说明对jQuery的依赖。

let responseToImages = (resp)=> {
  let urls = resp.items.map((item)=> item.media.m )
  let images = urls.map((url)=> $('<img />', { src: url }))

  return images
}

我们的功能现在只做两件事:

  • 映射响应data -> urls
  • 映射urls -> images

实现此目的的“功能”方法是为这两个任务创建单独的功能,我们可以使用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所述。

拥有大量基本功能的程序员,更重要的是,他们掌握如何使用它们的知识,比从头开始的程序员更有效。 – 雄辩的JavaScript,Marijn Haverbeke

这是每个JavaScript开发人员都应学习和掌握的基本功能的列表。 这也是提高您的JavaScript技能以从头开始编写每个功能的好方法。

数组

功能

少即是多

让我们看一下可以使用功能性编程概念来改进以下代码的一些实际步骤。

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[0];
    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 = (items)=> items.map(upperCase)

删除代码,直到停止工作

对于这样一个简单的任务,我们根本不需要功能,该语言为我们提供了足够的抽象来逐字写出。

let items = ['a', 'b', 'c']
let upperCaseItems = items.map((item)=> item.toUpperCase())

测试中

能够简单地测试我们的程序是纯函数的主要优势,因此,在本节中,我们将为我们之前看过的Flickr模块设置一个测试工具。

启动终端并准备好您的文本编辑器,我们将使用Mocha作为我们的测试运行器,并使用Babel来编译我们的ES6代码。

mkdir test-harness
cd test-harness
npm init -yes
npm install mocha babel-register babel-preset-es2015 --save-dev
echo '{ "presets": ["es2015"] }' > .babelrc
mkdir test
touch test/example.js

Mocha有很多方便的功能,例如describeit用于破坏我们的测试和挂钩,例如before设置和拆卸任务beforeafterassert是一个核心节点程序包,可以执行简单的相等性测试, 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)

繁荣。

注意:如果希望mocha监视更改并自动运行测试,也可以在此命令的末尾添加-w标志,它们在重新运行时将以更快的速度运行。

mocha --compilers js:babel-register --recursive -w

测试我们的Flickr模块

让我们将模块添加到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)=> response.json())
  .then(responseToImages)
}

export default {
  _responseToImages: responseToImages,
  flickr: flickr,
}

我们的模块公开了两种方法:公开使用的flickr和私有函数_responseToImages以便我们可以_responseToImages进行测试。

我们有几个新的依赖项: jqueryunderscore和polyfills用于fetchPromise 。 为了测试这些,我们可以使用jsdomjsdom DOM对象windowdocument ,并且可以使用sinon包对获取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 ,在这里我们将在给定预定义输入的情况下对函数输出进行断言。 我们“存根”或重写全局获取方法来拦截和伪造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' }
  }]
}

// In a real project we'd shift this test helper into a module
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', ()=> {
    // Intercept calls to fetch(url) and return a Promise
    before(()=> {
      sinon.stub(global, 'fetch', (url)=> {
        return jsonResponse(sampleResponse)
      })
    })

    // Put that thing back where it came from or so help me!
    after(()=> {
      global.fetch.restore();
    })

    it("returns a Promise that resolves 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)

! 我们已经成功测试了我们的小模块及其组成的功能,学习了纯功能以及如何使用功能组合。 我们已经将纯净与不纯净分离开了,它易于读取,由小的功能组成,并且经过了良好的测试。 上面不合理的纯示例相比该代码更易于阅读,理解和修改 ,这是重构代码时我的唯一目的。

纯函数,请使用它们。

目前为止就这样了! 感谢您的阅读,我希望您对JavaScript中的函数式编程,重构和测试有很好的介绍。 目前,这是一个有趣的范例,主要是由于鼓励或执行这些模式的ReactReduxElmCycleReactiveX等库的日益流行。

跳进去,水是温暖的。

From: https://www.sitepoint.com/an-introduction-to-reasonably-pure-functional-programming/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值