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

Xekin發表於2018-11-26

備註:本篇本章是“組合式軟體程式設計”中的一部分,從基礎開始學習 JavaScript ES6+ 的函數語言程式設計和組合軟體技術。更多的內容請保持關注我們。 < 上一章節 | << 返回第一章節 | 下一章節 >

隨著我在程式開發中愈加成熟,我愈加重視底層的原理 —— 這是在我還是個初學者時所被我所忽視的,但現在隨著開發經驗越來越豐富,這些基礎的原理也具有了深厚的意義。

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

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

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

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

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

所有軟體都是抽象的,在我們獲利的同時,也將所有的辛勤工作以及那些無意識的細節埋藏。

軟體的執行過程大多都是不停的重複著。如果在問題分解階段,我們決定一遍又一遍地重複實現相同的功能,將會造成大量不必要的工作。至少這樣做肯定是愚蠢的。在許多情況下,這都是不切實際的。

相反,我們可以通過編寫一些對應的元件(像是函式、模組、類等等),再給個名稱作為標識,然後我們就可以在需要使用它們的地方再去複用它們。

分解的過程就是抽象的過程。成功的抽象也就意味著結果是一組可以單獨使用並且也可以重新組合的元件。由此我們瞭解了一個非常重要的軟體架構原則:

軟體解決方案應該可以被分解為其元件部分,並且可以重新組合成為新的解決方案,而無需更改內部的元件實現細節。

抽象是一種簡化的行為

“簡化就是將顯而易見的東西減去並增添有意義的東西” ~ 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前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章