搜尋
首頁php教程PHP开发合理的使用純函數式編程

函數式程式設計能夠降低程式的複雜程度:函數看起來就像是一個數學公式。學習函數程式設計能夠幫助你寫簡單且更少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 $(&#39;<img  / alt="合理的使用純函數式編程" >&#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");

花一分鐘理解上面的程式碼。

除非你接觸過函數式程式設計的這些概念(柯里化,組合和prop),否則很難理解上述程式碼。相較於純函數式的方法,下面的程式碼則更容易理解和修改,它更清晰的描述程式並且更少的程式碼。

app函數的參數是一個標籤字串

從Flickr獲取JSON資料

從返回的資料中抽出urls

創建合理的使用純函數式編程節點數組

將他們插入文檔來更好的進行非同步操作。

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  / alt="合理的使用純函數式編程" >&#39;, {src:url}) );
    
    $(document.body).html(images);
  })
}
app("cats");

Ajax請求和DOM操作都不是純的,但是我們可以將餘下的操作組成純函數,將傳回的JSON資料轉換成圖片節點數組。

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  / alt="合理的使用純函數式編程" >&#39;, { src: url }) )

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

我們的函數做了2件事:

將傳回的資料轉換成urls

🎜將urls轉換成圖片節點🎜🎜函數式的方法是將上述2個任務拆開,然後使用compose將一個函數的結果作為參數傳給另一個參數。 🎜
let responseToImages = (resp) => {
  let urls = resp.items.map((item) => item.media.m)
  let images = urls.map((url) => $(&#39;<img  / alt="合理的使用純函數式編程" >&#39;, {src:url}))
  
  return images
}
🎜compose 傳回一系列函數的組合,每個函數都會將後一個函數的結果作為自己的入參🎜🎜這裡compose做的事情,就是將urls的結果傳入images函數🎜
let urls = (data) => {
  return data.items.map((item) => item.media.m)
}
let images = (urls) => {
  return urls.map((url) => $(&#39;<img  / alt="合理的使用純函數式編程" >&#39;, {src: url}))
}
let responseToImages = _.compose(images, urls)
🎜透過將程式碼變成純函數,讓我們在以後有機會重複使用他們,他們更容易被測試和自我文件化。不好的是當我們過度的使用這些函數抽象(像第一個例子), 就會使事情變得複雜,這不是我們想要的。當我們重構程式碼的時候最重要的是要問自己:🎜🎜這是否讓程式碼更容易閱讀和理解? 🎜

基本功能函数

我并不是要诋毁函数式编程。每个程序员都应该齐心协力去学习基础函数,这些函数让你在编程过程中使用一些抽象出的一般模式,写出更加简洁明了的代码,或者像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  / alt="合理的使用純函數式編程" >&#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  alt="合理的使用純函數式編程" >", () => {
      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  alt="合理的使用純函數式編程" >", (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  alt="合理的使用純函數式編程" >
  .flickr
    ✓ returns a Promise that resolves with a NodeList of <img  alt="合理的使用純函數式編程" >

3 passing (67ms)

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

陳述
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
1 個月前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
1 個月前By尊渡假赌尊渡假赌尊渡假赌
威爾R.E.P.O.有交叉遊戲嗎?
1 個月前By尊渡假赌尊渡假赌尊渡假赌

熱工具

Atom編輯器mac版下載

Atom編輯器mac版下載

最受歡迎的的開源編輯器

MantisBT

MantisBT

Mantis是一個易於部署的基於Web的缺陷追蹤工具,用於幫助產品缺陷追蹤。它需要PHP、MySQL和一個Web伺服器。請查看我們的演示和託管服務。

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用