Home >php教程 >PHP开发 >Use pure functional programming wisely

Use pure functional programming wisely

高洛峰
高洛峰Original
2016-11-22 12:25:551221browse

Functional programming can reduce the complexity of the program: a function looks like a mathematical formula. Learning functional programming can help you write simpler code with fewer bugs.

Pure function

A pure function can be understood as a function that must have the same output with the same input, without any observable side effects

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

The above is a pure function, it does not depend on or change any variables other than the function state, always returns the same output for the same input.

//impure
var minimum = 21;
var checkAge = function(age) {
  return age >= minimum; // 如果minimum改变,函数结果也会改变
}

This function is not a pure function because it relies on external mutable state

If we move the variable inside the function, then it becomes a pure function, so that we can ensure that the function can be compared correctly every time age.

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

Pure functions have no side effects, some things you need to remember is that it does not:

Access system state outside the function

Modify objects passed as parameters

Initiate http requests

Preserve user input

Querying the DOM

Controlled mutation

You need to pay attention to some mutation methods that will change arrays and objects. For example, you need to know the difference between splice and slice.

//impure, splice 改变了原数组
var firstThree = function(arr) {
  return arr.splice(0,3);
}

//pure, slice 返回了一个新数组
var firstThree = function(arr) {
  return arr.slice(0,3);
}

If we avoid using mutator methods on the objects passed into the function, our programs will be easier to understand, and we can reasonably expect that our functions will not change anything outside the function.

let items = ['a', 'b', 'c'];
let newItems = pure(items);
//对于纯函数items始终应该是['a', 'b', 'c']

Advantages of pure functions

Compared to impure functions, pure functions have the following advantages:

Easier to test, because their only responsibility is to calculate output based on input

The results can be cached, because the same Input will always get the same output

Self-documenting because the dependencies of the function are clear

Easier to call because you don’t have to worry about the side effects of the function

Because the results of pure functions can be cached, we can remember Keep them in place so that complex and expensive operations only need to be performed once when called. For example, caching the results of a large query index can greatly improve program performance.

Unreasonable pure function programming

Using pure functions can greatly reduce the complexity of the program. However, if we use too many abstract concepts of functional programming, our functional programming will also be very difficult to understand.

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 $(&#39;<img />&#39;, { src: url });
};

var url = function (t) {
  return &#39;http://api.flickr.com/services/feeds/photos_public.gne?tags=&#39; +
    t + &#39;&format=json&jsoncallback=?&#39;;
};

var mediaUrl = _.compose(_.prop(&#39;m&#39;), _.prop(&#39;media&#39;));
var mediaToImg = _.compose(img, mediaUrl);
var images = _.compose(_.map(mediaToImg), _.prop(&#39;items&#39;));
var renderImages = _.compose(Impure.setHtml("body"), images);
var app = _.compose(Impure.getJSON(renderImages), url);
app("cats");

Take a minute to understand the above code.

Unless you are exposed to these concepts of functional programming (currying, composition and props), it will be difficult to understand the above code. Compared with the purely functional approach, the following code is easier to understand and modify, it describes the program more clearly and requires less code.

The parameter of the app function is a tag string

Get JSON data from Flickr

Extract urls from the returned data

Create a1f02c36ba31691bcfe87b2722de723b node array

Insert them into the document

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) => $(&#39;<img />&#39;, {src:url}) );
    
    $(document.body).html(images);
  })
}
app("cats");

Or you can use fetch and Promise to better perform asynchronous operations.

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)=> $(&#39;<img />&#39;, { src: url }) )

      return images
  })
}
flickr("cats").then((images)=> {
  $(document.body).html(images)
})

Ajax requests and DOM operations are not pure, but we can form the remaining operations into pure functions and convert the returned JSON data into an array of image nodes.

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

Our function does 2 things:

Convert the returned data into urls

Convert urls into image nodes

The functional approach is to split the above 2 tasks, and then use compose to combine a function The result is passed as a parameter to another parameter.

let urls = (data) => {
  return data.items.map((item) => item.media.m)
}
let images = (urls) => {
  return urls.map((url) => $(&#39;<img />&#39;, {src: url}))
}
let responseToImages = _.compose(images, urls)

compose returns a combination of functions, each function will use the result of the latter function as its own input parameter

What compose does here is to pass the result of urls into the images function

let responseToImages = (data) => {
  return images(urls(data))
}

By changing the code into Making them pure functions gives us the opportunity to reuse them in the future, and they are easier to test and self-documenting. The bad thing is that when we overuse these functional abstractions (like in the first example), it complicates things, which is not what we want. The most important thing to ask yourself when we refactor code is:

Does this make the code easier to read and understand?

基本功能函数

我并不是要诋毁函数式编程。每个程序员都应该齐心协力去学习基础函数,这些函数让你在编程过程中使用一些抽象出的一般模式,写出更加简洁明了的代码,或者像Marijn Haverbeke说的

一个程序员能够用常规的基础函数武装自己,更重要的是知道如何使用它们,要比那些苦思冥想的人高效的多。--Eloquent JavaScript, Marijn Haverbeke

这里列出了一些JavaScript开发者应该掌握的基础函数
Arrays
-forEach
-map
-filter
-reduce

Functions
-debounce
-compose
-partial
-curry

Less is More

让我们来通过实践看一下函数式编程能如何改善下面的代码

let items = [&#39;a&#39;, &#39;b&#39;, &#39;c&#39;];
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 = [&#39;a&#39;, &#39;b&#39;, &#39;c&#39;]
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 &#39;{ "presets": ["es2015"] }&#39; > .babelrc
mkdir test
touch test/example.js

Mocha提供了一些好用的函数如describe和it来拆分测试和钩子(例如before和after这种用来组装和拆分任务的钩子)。assert是用来进行相等测试的断言库,assert和assert.deepEqual是很有用且值得注意的函数。

让我们来编写第一个测试test/example.js

import assert from &#39;assert&#39;;

describe(&#39;Math&#39;, () => {
  describe(&#39;.floor&#39;, () => {
    it(&#39;rounds down to the nearest whole number&#39;, () => {
      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 &#39;jquery&#39;;
import { compose } from &#39;underscore&#39;;

let urls = (data) => {
  return data.items.map((item) => item.media.m)
}

let images = (urls) => {
  return urls.map((url) => $(&#39;<img />&#39;, {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(&#39;jsdom&#39;).jsdom(&#39;<html></html>&#39;);
global.window = document.defaultView;
global.$ = require(&#39;jquery&#39;)(window);
global.fetch = require(&#39;whatwg-fetch&#39;).fetch;

我们的测试代码在test/flickr.js,我们将为函数的输出设置断言。我们"stub"或者覆盖全局的fetch方法,来阻断和模拟HTTP请求,这样我们就可以在不直接访问Flickr api的情况下运行我们的测试。

import assert from &#39;assert&#39;;
import Flickr from &#39;../lib/flickr&#39;;
import sinon from &#39;sinon&#39;;
import { Promise } from &#39;es6-promise&#39;;
import { Response } from &#39;whatwg-fetch&#39;;

let sampleResponse = {
  items: [{
    media: { m: &#39;lolcat.jpg&#39; }
  }, {
    media: {m: &#39;dancing_pug.gif&#39;}
  }]
}

//实际项目中我们会将这个test helper移到一个模块里
let jsonResponse = (obj) => {
  let json = JSON.stringify(obj);
  var response = new Response(json, {
    status: 200,
    headers: {&#39;Content-type&#39;: &#39;application/json&#39;}
  });
  return Promise.resolve(response);
}


describe(&#39;Flickr&#39;, () => {
  describe(&#39;._responseToImages&#39;, () => {
    it("maps response JSON to a NodeList of <img>", () => {
      let images = Flickr._responseToImages(sampleResponse);
      
      assert(images.length === 2);
      assert(images[0].nodeName === &#39;IMG&#39;);
      assert(images[0].src === &#39;lolcat.jpg&#39;);
    })
  })
  
  describe(&#39;.flickr&#39;, () => {
    //截断fetch 请求,返回一个Promise对象
    before(() => {
      sinon.stub(global, &#39;fetch&#39;, (url) => {
        return jsonResponse(sampleResponse)
      })
    })
    
    after(() => {
      global.fetch.restore();
    })
    
    it("returns a Promise that resolve with a NodeList of <img>", (done) => {
      Flickr.flickr(&#39;cats&#39;).then((images) => {
        assert(images.length === 2);
        assert(images[1].nodeName === &#39;IMG&#39;);
        assert(images[1].src === &#39;dancing_pug.gif&#39;);
        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)

到这里,我们已经成功的测试了我们的模块以及组成它的函数,学习到了纯函数以及如何使用函数组合。我们知道了纯函数与不纯函数的区别,知道纯函数更可读,由小函数组成,更容易测试。相比于不太合理的纯函数式编程,我们的代码更加可读、理解和修改,这也是我们重构代码的目的。

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn