[譯] 函數式編程:抽象與組合(系列教程第十五部分)

備註:本篇本章是「組合式軟件編程」中的一部分,從基礎開始學習 JavaScript ES6+ 的函數式編程和組合軟件技術。更多的內容請保持關注咱們。 < 上一章節 | << 返回第一章節 | 下一章節 >javascript

隨着我在程序開發中越發成熟,我越發重視底層的原理 —— 這是在我仍是個初學者時所被我所忽視的,但如今隨着開發經驗愈來愈豐富,這些基礎的原理也具備了深厚的意義。前端

「在空手道中,黑帶的驕傲象徵是從黑帶穿到褪色而變爲白帶,這象徵着回到了最初的狀態」 ~ John Maeda,「簡化的法則:設計,技術,商業,生活」java

在 Google 詞典中寫着,抽象是「獨立於事物的關聯、屬性或具體附屬物來考慮事物的過程」。android

抽象的詞源來自中世紀拉丁語 abstractus,意爲「拽開、抽離」。我喜歡這樣的解讀。抽象意味着移除某些東西 —— 但到底咱們移除掉了什麼,又爲了什麼目的呢?ios

有時我喜歡將詞彙翻譯成其餘語言而後再把它們翻譯回英文,站在不一樣的角度去思考咱們在英語中沒有想到過的其餘聯想。當我把「抽象」一詞翻譯爲意第緒語再翻譯回英語時,結果意思是「心不在焉的」,我也喜歡這樣的答案。一個心不在焉的人在使用自動駕駛儀的時候,不會去主動思考駕駛儀在作什麼...只是這樣作。git

抽象讓咱們得以安全的使用自動駕駛儀。全部軟件都是自動化的。若是你有足夠的時間,你在電腦上作的任何事情也均可以用紙,墨水,再加上信鴿來作。軟件就只是把這些手動作起來十分耗時的全部細節自動化處理了。github

全部軟件都是抽象的,在咱們獲利的同時,也將全部的辛勤工做以及那些無心識的細節埋藏。算法

軟件的運行過程大多都是不停的重複着。若是在問題分解階段,咱們決定一遍又一遍地重複實現相同的功能,將會形成大量沒必要要的工做。至少這樣作確定是愚蠢的。在許多狀況下,這都是不切實際的。編程

相反,咱們能夠經過編寫一些對應的組件(像是函數、模塊、類等等),再給個名稱做爲標識,而後咱們就能夠在須要使用它們的地方再去複用它們。後端

分解的過程就是抽象的過程。成功的抽象也就意味着結果是一組能夠單獨使用而且也能夠從新組合的組件。由此咱們瞭解了一個很是重要的軟件架構原則:

軟件解決方案應該能夠被分解爲其組件部分,而且能夠從新組合成爲新的解決方案,而無需更改內部的組件實現細節。

抽象是一種簡化的行爲

「簡化就是將顯而易見的東西減去並增添有意義的東西」 ~ John Maeda,「簡化的法則:設計,技術,商業,生活」

抽象過程主要有兩個組成部分:

  • 泛化是在重複模式中找到類似的(並顯而易見的)功能並經過抽象來將它們隱藏的一個過程。
  • 特殊化是在使用抽象時,爲那些只在某處不一樣(且有其特殊意義的)提供用例。

抽象是一個提取概念本質的過程。經過發現不一樣領域中不一樣問題的共同點,咱們能夠認識到若是跨出本身的視界從不一樣的角度去看待問題。當咱們看到問題的本質時,咱們就能夠找出一個好的解決方案同時它也能夠適用於許多其餘問題。若是咱們將這樣的思想應用在代碼上,咱們就能夠從根本上下降應用程序的複雜性。

「若是你願意觸碰事物的深層基礎,你將觸碰到它的一切。」 ~ Thich Nhat Hanh

此原則可用於從根本上減小構建應用程序所需的代碼。

軟件中的抽象

軟件中的抽象有不少種形式

  • 算法
  • 數據解構
  • 模塊
  • 框架

而我我的最喜歡的是:

「有時,優雅的實現僅僅是一個函數。而不是一種方法。也不是類。也不是框架。只是一個函數而已。」 ~ John Carmack (Id Software, Oculus VR)

函數具備很好的抽象性,由於它們自己具備良好抽象所具有的特性:

  • 標識性 — 爲其分配名稱並在不一樣的上下文當中重複使用。
  • 可組合性 — 能夠將簡單的函數組合成更復雜的函數。

組合抽象

在軟件中最經常使用於抽象的函數莫過於純函數,它與數學中的函數有着相同的模塊化特徵。在數學中,一個函數對於相同的輸入值,永遠會獲得相同的輸出。咱們能夠將函數視爲輸入和輸出之間的關係。給定一些輸入 A,一個函數 f 將會產生 B 做爲輸出。你能夠說是 f 定義了 AB 之間的關係:

f: A -> B
複製代碼

一樣的,咱們能夠定義另外一個函數,g,它則定義了 BC 之間的關係:

g: B -> C
複製代碼

意味着另外一個函數 h 就直接定義了 AC 之間的聯繫:

h: A -> C
複製代碼

這些關係構成了問題空間的結構,也由此你在應用程序中組合函數的方式也就構成了應用程序的結構。

將這些結構隱藏起來,一個良好的抽象就誕生了,一樣的方式咱們使用 h 這個方法就能夠將 A -> B -> C 這個過程縮減爲 A -> C

如何用更少的代碼作更多的事情

抽象是用更少代碼作更多事的關鍵。舉個例子,假如你寫一個函數用來計算兩個數字相加:

const add = (a, b) => a + b;
複製代碼

可是你常常將它用於遞增,所以固定其中一個數字是合理的:

const a = add(1, 1);
const b = add(a, 1);
const c = add(b, 1);
// ...
複製代碼

咱們能夠柯里化這個方法:

const add = a => b => a + b;
複製代碼

而後建立一個偏函數應用,在函數調用時傳入第一個參數,就會返回一個接受下一個參數的新函數:

const inc = add(1);
複製代碼

如今,當咱們須要增長 1 時,咱們可使用 inc 而不是以前的 add 方法,這就減小了咱們所需的代碼量:

const a = inc(1);
const b = inc(a);
const c = inc(b);
// ...
複製代碼

在這個例子裏,inc 只是用來完成相加運算的一個特定版本。全部柯里化函數都是抽象出來的。而在實際上,全部高階函數均可以歸納爲經過傳遞一個或者多個參數來獲得特定的結果。

好比 Array.prototype.map() 就是一個高階函數,它抽象出一個方案,用來將函數應用於數組當中的每一個元素以返回處理後所獲得的元素構成的新數組。咱們能夠將 map 寫成一個柯里化函數來讓這個過程更加的明顯:

const map = f => arr => arr.map(f);
複製代碼

這版代碼中的 map 是接受一個特定函數做爲參數,而後返回另外一個特定的方法,即以給定函數爲方法,處理數組中每一個元素:

const f = n => n * 2;

const doubleAll = map(f);
const doubled = doubleAll([1, 2, 3]);
// => [2, 4, 6]
複製代碼

注意這裏咱們定義 doubleAll 僅僅只須要這一小段代碼 map(f) —— 就這麼簡單!這就是它的整個定義。若是咱們在開始構建咱們的代碼塊時就抽象那些經常使用的功能,咱們就能夠用不多的新代碼來組合成至關複雜的行爲。

結論

軟件開發人員花費它們的整個職業生涯來建立抽象和組合抽象 —— 但仍有許多人對抽象或者組合它們沒有一個良好的基本掌握。

每當你建立抽象時,你都應該仔細地去考慮它,並且你也應該要意識到有不少已經爲你提供地良好抽象(例如經常使用的 mapfilterreduce)。咱們應該要學會識別抽象的特徵:

  • Simple(簡單)
  • Concise(明瞭)
  • Reusable(可重用的)
  • Independent(獨立的)
  • Decomposable(可分解的)
  • Recomposable(可從新組合的)

EricElliottJS.com 瞭解更多信息

更多關於函數式編程的視頻課程可供 EricElliottJS.com 的會員使用。若是您還不是會員,請當即註冊


Eric Elliott 是 「Programming JavaScript Applications」(O'Reilly)的做者,也是軟件導師平臺 DevAnywhere.io 的聯合創始人。他爲 Adobe Systems、Zumba Fitness、華爾街日報、ESPN、BBC 以及包括 Usher 和 Frank Oc 等在內的頂級錄音藝術家的軟件體驗作出了貢獻。

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索