如何編寫高質量的函式 -- 打通任督二脈篇[理論卷]

原始碼終結者發表於2019-03-26

如何編寫高質量的函式 -- 打通任督二脈篇[理論卷]

3月的春風

凡是點進來的老鐵都會受到 dva 的賣萌祝福。我會理論結合實踐的去闡述:如何運用函數語言程式設計思想去編寫高質量的函式。 在這篇文章中,你可以收穫一個滿意的答案。

有時候,世間的答案並不重要,重要的是,你如何去看待和相信這份答案。

理論卷看完的小夥伴可以點選下面連結,開啟實戰卷:

如何編寫高質量的函式 -- 打通任督二脈篇[實戰卷]

這部分是寫到1萬字後加的

嗯,本來打算一篇搞定,可是寫著寫著就到了 10000 字了。雖然已經較簡潔了,但是涉及到的知識有點多,還是要花字數去闡述清楚的。於是乎,我決定分為上下兩篇來完成打通 任督二脈篇 這個篇章。

如何寫這篇文章

這是我編寫高質量函式系列的第三篇文章 -- 打通任督二脈。

前兩篇文章分別為:

如何編寫高質量的函式 -- 敲山震虎篇

如何編寫高質量的函式 -- 命名/註釋/魯棒篇

第三篇是關於 如何運用函數語言程式設計來編寫高質量函式 的一篇文章。

關於函數語言程式設計,我研究了很長時間,這裡我想結合如何編寫高質量函式,來寫出和其他函數語言程式設計文章不一樣的 feel

本著不重複造輪子,只造更好的輪子的原則,在寫這篇文章之前,我思考了一會兒,我發現自己面臨兩個問題,這裡我簡單說一下,問題和答案如下:

我寫文章時面臨的問題

第一個問題:如何高質量的闡述函數語言程式設計

第二個問題:如何真正意義上的通過打通函式式的任督二脈來提高編寫函式的質量

我的答案

第一個問題的答案

我會通過闡述幾個很細節但非常重要的函數語言程式設計知識,通過這些細節來介紹函數語言程式設計,同時向大家展示函數語言程式設計的知識體系,以及和我們現在大部分的知識體系有什麼共同點和不同點。

第二個問題的答案

這裡我會通過分析開源專案的程式碼和平時工作中的程式碼以及小夥伴的程式碼,來展示如何運用函數語言程式設計來提高函式的質量。

上述是我面臨的一些問題和解決方案,最後寫出了這兩篇文章,希望能夠獲得大家的喜歡吧。(當然嘍,拉勾打個賭,敢不敢不看完文章就先給我點個贊,嘿嘿嘿)

番外篇——當代前端工程師面臨的考驗

突然的番外,讓我措手不及,來不及思考,就要進入新世界了。

需求的複雜度越來越高

我想說的是:由於前端需求的複雜度越來越高,大量的業務邏輯放到前端去實現,這雖然減輕了後端的壓力,但是卻讓前端的壓力大大增加,前端工程師為此要完成儲存、非同步、事件等一系列綜合性的操作。

需求完成度越來越苛刻

我們可以看一個公式:

f(需求) = 完美

簡單理解就是:輸入需求,然後輸出儘可能完美的結果。

在需求複雜度持續增加的條件下,需求完成度卻越來越苛刻,這樣的情況,會導致前端工程師的壓力很大。我們不僅要解決各種負責的業務場景,還要保證還原度和開發效率,以及線上執行的效能等方面的表現。

前端工程師的普遍目標

我總結了一下,大致有四點:

  1. 增強開發技能
  2. 提高程式碼質量
  3. 提高開發效率
  4. 享受生活和 coding 的樂趣

這四點總結起來,我們要達成的目的,其實就是下面這張圖:

如何編寫高質量的函式 -- 打通任督二脈篇[理論卷]

我能做的就是提前告訴你 One Piece 的內容。嗯,沒錯,就是上面這張圖。加油吧,各位兄得。

前端工程師的普遍現狀

但現實總是美好和殘酷混合的,由於各種原因,前端工程師的普遍現狀可以用下面幾點概況。

  1. coderoop 思想不強,導致程式碼質量不高
  2. coderfp 思想不強,導致程式碼質量不高
  3. JS 語言自身的問題,導致程式碼的可控性較差

為什麼我會這樣說呢,且聽我娓娓道來

首先,JS 是一門很靈活的程式語言,在靈活和基礎不牢的雙重作用下,程式碼就會積累足夠的複雜度,程式碼的質量和可控性就會出現問題。

關於 oopfp 的思想,可以用下面兩句話進行總結概況

面對物件程式設計 oop 通過封裝變化,使得程式碼質量更高。

函數語言程式設計 fp 通過最小化變化,使得程式碼質量更高。

而前端工程師的 oopfp 思想都不是很成熟,雖然現在前端界一直在推動吸收這些思想,但目前確實還是一個問題。

我個人的看法和總結

對於上面我寫的那些話,我不是說前端工程師底子差什麼的,我是在客觀闡述一些觀點,大家可以想一下,為什麼現在不去說 PHP 了,還不是因為愛你( 前端工程師 )啊。

我們就像是新世代被選中的一群孩子,能做的就是接受這份挑戰。就算以後某個時刻,你不做 coder 了 ,但是現在,身為前端 coder 的你,應該去戰勝它,不為別的,只為作為 coder 心中的那一份執著和榮耀。

科普篇--關於程式語言

你跟我來,我從番外篇穿梭到科普篇的世界裡。

JavaScript 是函數語言程式設計語言嗎

要回答這個問題,首先,要明確的一點是:

JS 不是純正的函數語言程式設計語言,所以它不是函數語言程式設計語言。

為什麼呢?

因為它具有 for 迴圈等命令式味道過重的語言設計。真正的純函數語言程式設計語言,比如 Haskell 是沒有 for 迴圈的,遍歷操作都是使用遞迴。

為什麼我要提這個點,是因為我想說一個道理,那就是:

既然 JavaScript 不是純正的函數語言程式設計語言,那我們就沒有必要在使用 JS 的時候,全部採用 FP 思想,我們要做的,應當是取長補短。其實這句話也從另一個方面結束了之前掘金上關於函數語言程式設計的討論。

那 JavaScript 是什麼語言?

這裡我引用一句話:

自然語言是沒有主導正規化的,JavaScript 也同樣沒有。開發者可以從過程式、函式式和麵向物件的大雜燴中選擇自己需要的,再適時的地把它們融為一體。

出自:Angus Croll, 《If Hemingway Wrote JavaScript》

聽我分析,雖然上面的話可能過於絕對,如果想知道上面的話為什麼會這樣說,我建議大家可以去學一下編譯原理知識。

為什麼我要這樣建議

是因為我個人認為,所有的高階語言,都是要通過翻譯來變成機器語言的,而越底層的實現,其思想和方法越具體和單一。比如,詞法分析,語法分析,業界的方法屈指可數。也是因為只有統一,才能建立標準,就像當今的 TCP/IP 協議。

當然,我胡謅那麼多沒有啥卵用,但我們依然可以知道並確定一件事情,那就是:

討論 JavaScript 是一門基於什麼樣的語言,是沒有任何意義的。

一等公民究竟是什麼鬼哦

都是 JS 的函式是一等公民,那一等公民究竟是什麼鬼哦?其實,程式語言也是分 階級 的,大致有如下階級:

一等公民

如果程式語言中的一個值,可以作為引數傳遞,可以從程式中返回,可以賦值給變數,就稱它為一等公民。

二等公民

可以作為引數傳遞,但是不能從子程式中返回,也不能賦給變數。

三等公民 它的值連作為引數傳遞都不行。

由於 JS 的函式是一等公民,所以我們可以把函式當初引數,也可以作為返回值,也可以賦值給變數。也正因為是一等公民,JS 的函數語言程式設計才能發光發熱。

賣萌篇--函式的分類

嗯,是不是很氣,仁兄別急,dva 現在拉著你的手,從科普片場來到了賣萌片場。

下面是我心血來潮,總結加胡謅的一些看法。

從純潔性角度分類

  1. 純函式
  2. 非純函式

嗯,我這分類沒誰了,就像人,可以分為是人和不是人...

從呼叫形式角度分類

中綴函式

程式碼如下:

6 * 6 
複製程式碼

6 * 6 中的 * 就是一個函式。而且由於在兩個表示式的中間,所以叫中綴。看到這,大家應該有這樣的思維,那就是在 JS 中用這些符號的時候,要自然的將其想象成函式。這樣你會發現程式碼會變得更容易理解。

字首函式

程式碼如下:

function f(str) {
  console.log(str)
}

f('hello, i am 原始碼終結者')
複製程式碼

這種在函式名 f 後面加上引數的執行形式,就稱函式 f 為字首函式。這個我就不說了,大家應該很熟悉了。

提一下 let

當我學習 haskell 的時候,我發現 haskell 也有 let 。它是這樣介紹的:

let 後加上表示式,可以允許你在任何位置定義區域性變數。

當看到這,我立刻想起來 JS 中 的 let 。這和 JSlet 不謀而合,雖然其他語言也有 let ,比如 go 語言。但我現在更有信心確定, JSlet 的誕生,借鑑的應該是 haskell 中的 let

所以,在 JS 中,我更想將 let 稱為繫結。haskell 中介紹了 let 的缺點,這裡我們推一下可以知道,JS 中的 let 缺點也很明顯,那就是 let 繫結的作用域被限制的很小。

所以你會發現,在某些場景下,使用 var 也是非常合理和正確的。

我是分割線,熱身要結束了,即將開始正題。


文章整體思路

當失去以後,再次擁有時,才會倍加珍惜吧。

嗯,那開始正題吧。

首先可以知道的一點是:本文的思路是清晰的,文章前半段,我會著重闡述函數語言程式設計的理論知識。比如我會闡述很多人心中對於函數語言程式設計的一些困惑。文章後半段,我會結合開源專案的原始碼和實際工作中的 coding ,來展示如何運用函數語言程式設計來編寫高質量的函式。

下文的 函數語言程式設計 統稱為 FP

關於 FP 你真正困惑的那些點--FP理論知識

關於理論知識,我不會把 FP 的方方面面都涵蓋,我會嘗試從小的點出發,帶你去認識 FP ,讓大家可以在巨集觀層面上對函數語言程式設計有一個較清晰的認知。

下面,我會從以下這些點出發:

  • 函數語言程式設計存在的意義
  • 宣告式程式設計
  • 數學中的函式
  • 純函式/純潔性/等冪性/引用透明/
  • 副作用
  • 資料的不可變性
  • 惰性
  • 態射
  • 高階性
  • 其他理論知識

PS: 這裡我不會對單個點進行詳細介紹,只說出我個人對其的一個認知和理解。

函數語言程式設計存在的意義

這是一本書中的解釋:

函數語言程式設計的目的是使用函式來抽象作用在資料之上的控制流與操作,從而在系統中消除副作用並減少對狀態的改變。

這句話總結的很不錯,看起來好像很深奧,小夥伴們不要怕,看下面我的分析你就明白了。

我們看一下上面的解釋,會發現這句話,其實是在說:

函數語言程式設計是一種解決問題的方式。

為什麼我會這樣說呢,繼續往下看:

我們繼續分析上面的解釋,可以知道下面兩點:

第一點:FP 抽象了過程,比如控制流(如 for 迴圈)

第二點:FP 消除程式中的副作用(後面說)

稍加推導可以知道第三點:

FP 的存在一定是解決了其他正規化的一些缺陷,比如面對物件正規化的一些缺陷,如副作用在面對物件中很常見,從 Java 的鎖機制可以看出副作用的表現,當然我對鎖機制這種觀點可能不準備,但是關於一定是解決了其他正規化的一些缺陷這個觀點,還是很正確的,不然就沒有存在的意義了。

提一下函數語言程式設計和抽象的關係

上面第一點提到,FP 抽象了過程,關於抽象,我們知道,抽象層次越低,程式碼重用的難度就會越大,出現錯 誤的可能性就會越大。

為什麼說抽象層次越低,程式碼重用的難度就會越呢?

我來舉個栗子,你就會明白了。就一句話:每次吃麵條時,作為 coder 的你,有兩種選擇:

第一種:直接去超市買把麵條,下著吃。

第二種:自己和麵,自己擀麵條,然後下著吃。

上面兩種方法,你會選哪一種呢,其實我已經猜到了,抽象的高和低就是第一種和第二種的區別,小夥伴好好想一想。

綜上,我們可以知道:

FP 的本質意義就在於此,真正理解了其存在的目的,也就理解了我們為什麼要使用函數語言程式設計了。

前段日子掘金上關於 FP 的討論

之前掘金上有那麼幾天非常熱鬧,是關於函數語言程式設計的討論,有說函式式好用的,有說不用函式式的政治正確的,有 /^駁/ 的。那些天,每天刷掘金,都能看到相關的文章,總是想說些什麼,但是最後也沒有說什麼。

但是,如果真正理解了函數語言程式設計的意義,也就不會有這麼多不同的聲音了。

但聲音這麼多,主要原因應該還是:

寫文章的時候,偶爾會說出很絕對的話,這會導致其他作者進行迴應,緊接著,事件持續發酵,激起了可愛的吃瓜群眾的興趣,遂開啟了大規模的圍觀和討論。

在這裡,我想用一句話來結束爭議,那就是:

函式式不是工具,也不是 api ,它是一種解決問題的方式。

這句話,不做解釋了,自行腦補吧。

宣告式程式設計

說到宣告式,就要提到命令式。宣告式和命令式,是目前前端所面臨的兩種程式設計正規化。

小夥伴應該知道,函數語言程式設計是宣告式程式設計。當你看完上面我對 FP 意義的解釋後,你就會明白為什麼會有宣告式程式設計存在了。

FP 可以將控制流抽象掉,不要其暴露在程式中,這就是一種宣告式程式設計。

關於程式設計正規化,大家可以快速瀏覽下面關於程式設計正規化的維基:

[維基百科連結-程式設計正規化]zh.wikipedia.org/wiki/程式設計範型

思考題:宣告式程式設計的軟肋在哪,它與指令式程式設計的本質區別是什麼?小夥伴可以思考一下。

數學中的函式

為什麼要說這個,我認為,如果想更好的理解和運用函數語言程式設計,就必須要去了解數學中的函式,因為函數語言程式設計的很多概念和數學中函式的思想是強相關的。

這裡我不做分析了,我貼一個維基連結:

函式-維基

小夥伴們自行學習一下,看的時候,大家可以順帶滑到最下面,看看有相關範疇論的介紹,範疇學的思想對函數語言程式設計非常重要。

純函式/純潔性/等冪性/引用透明性

這幾個詞在 FP 中很常見,在我看來,這些詞最終的目標都是一樣的,那就是達成上文提到的 FP 的目的。這裡我用簡潔的語言去闡述一下這些詞背後的理念。

這些詞代表的意識其實是一樣的,可以這樣理解:

純函式的特性包括純潔性,等冪性,引用透明性。

那純函式是什麼呢?可以通過下面的說法來認識純函式。

所有的函式對於相同的輸入都將返回相同的輸出。

PS: 當然我這篇文章並不是讓大家運用純函式去實現程式設計,而是想讓大家運用函式式的思想去實現程式設計,不一定要是純函式的,引純函式知識 FP 的一個思想而已。

函式的副作用

上面說了純函式,那麼如何理解函式的副作用呢,可以這樣理解:

相同的輸入,一定會輸出相同的輸出,如果沒有的話,那就說明函式是有副作用的。

從這句話,我們認真想一下,可以推匯出:

因為 I/O 的輸出是具有不確定性的,所以一切的 I/O 都是有副作用的。

如何處理副作用

後面實戰再說,這裡我提一點,大家可以去看看 dva 的文件或者原始碼,其中關於非同步請求的,都只在 effect 中完成,而 effect 在函數語言程式設計中有副作用的意識,這是一種將函式的副作用隔離起來的設計思想。

資料的不可變性

關於資料的不可變性,還是很重要的,我們需要去深入的理解它。我想幾個問題:

第一:前端出現資料的不可變性的目的

第二:前端如何做到資料的不可變性

下面我們來一次回答這兩個問題。

前端出現資料的不可變性的目的

回答這個問題之前,我想說另外一個事情,那就是:許多程式語言都支援讓資料不可變,比如 javafinal ,可以看看這篇文章,很不錯。看完,你會對資料不可變有一個交叉的瞭解。

淺談Java中的final關鍵字

ok ,我個人認為,不可變性思想很簡單,但很重要,我舉個例子你就明白了。

程式碼如下:

const obj = {}
const arr = []
const sum = 0

function main(arg){
  // TODO:
}

main(obj)
main(arr)
main(sum)
複製程式碼

從上面的程式碼,我們可以知道以下幾點:

第一點:main 函式的引數都是依賴了外部作用域的變數

第二點:變數 obj arr sum 是可以被外界改變的

第三點:要注意一個事情,我們目前無法避免外界對這些變數進行改變,有可能是必須要改變的,也有可能是無意間被改變了。

第四點:雖然我們無法避免外界對其可能造成的影響,但我們可以避免我們自己對其可能造成的影響,比如我們使用 main 直接對 obj arr 本身進行操作,這樣的話就會使得 objarr 被改變,如果改變了,那在我們多次呼叫 main 函式時,就可能會出現問題。

第五點:上面的程式碼已經違反了函數語言程式設計的原則,就是不能依賴外界的資料,這樣會有副作用。

基於上面五點後的答案

知道上面五點的資訊後,我們想一下,如果能把arr obj sum 設定成不可變資料結構。那我們就可以直接避免了外界和我們本人對其造成的所有影響。因為設定了資料不可變性,外界沒法去改變它們。所以這就是為什麼,前端會出現資料不可變性這一說,因為當前端越來越複雜的時候,這種要求就表現的更加明顯了。但是,很遺憾的時候,JS 是不支援不可變的資料結構的。

前端如何做到資料的不可變性

前端如何做到資料的不可變性,這裡有三種方法:

第一種方法:

在函式內對引用型別建立一個新的副本,然後對副本進行指定業務處理。

第二種方法:

對引用型別進行封裝,業界常用的方法就是值物件模式,通過閉包的形式暴露出去。

第三種方法:

通過 JSapi 來實現,用規則來阻止物件狀態的修改。比如使用 Object.freeze() 來凍結一個物件,當外界試圖對這個物件進行增刪改屬性的時候,就會導致程式錯誤。對於第三個方法,我們看一下 react 的原始碼中,哪些地方用到了,如圖所示:

如何編寫高質量的函式 -- 打通任督二脈篇[理論卷]

ReactDOMComponent.js 中對 nextProp 物件進行了凍結,可能很多人不明白為什麼要凍結,其實很好理解,我們從因果關係來理解。

首先 react 假設 nextProp 不會發生改變,那麼怎麼保證不會發生改變呢?肯定是通過 Object.freeze(nextProp)nextProp 進行凍結,凍結後,react 就可以知道,從這個時刻開始,nextProp 物件就是不可變的了。所以它就可以進行下一步,按照不可變作為前置條件的流程了。這樣做,其實也是在告訴我們,如果我們試圖修改它,就會報錯。

資料的不可變性總結

理解不可變性是非常重要的,我們不能把不可變性框在一個很窄的範圍,比如上面的程式碼,全域性變數 sum 雖然是值型別,可能你覺得值型別是不可變的,但是你會發現,它也會被外界所修改。所以這裡的不可變性,並不是小範圍的不可變,而是多個層面的不可變,這裡還需要多多去理解和感悟吧。

惰性

我認為惰性不僅僅是 FP 的特性,它在前端領域,也是一個非常重要的思想。

惰性可以用一句話來解釋:

只在需要時才發生。

怎麼理解這句話呢,我來列舉一下,前端用到 惰性思想 的場景,場景如下:

  • 前端的懶載入
  • 前端的 tree shaking
  • 無限列表問題
  • recycle list
  • 動態匯入匯出
  • 前端的快取

想一下,會發現, FP 的惰性目的,已經包含在上面那些場景中了。

關於 只在需要時才發生。 這句話, 小夥伴們可以結合我列舉的場景進行對比理解,我就不繼續蒼白解釋了,點到為止。

態射

為什麼我要說這個,是因為理解態射的知識,對你更好的使用函數語言程式設計有著非常重要的意義。

那什麼是態射呢?

簡單理解,就是一種狀態到另一種狀態的對映。態射包含很多種形式,這裡我們說一下型別對映。比如輸入一個 String 型別,然後返回一個 Boolean 型別,這就屬於一種態射。

比如下面的 demo

const isNumber = n => typeof n === 'number'
複製程式碼

上面的函式,輸入的是數字型別,出入的是布林型別,這就是一種態射形式。函數語言程式設計的很多概念都和態射有關係,比如管道化、高階化、組合、鏈式等。如何將一種型別的資料機構對映成另一種資料結構,這是一個很重要的知識點,這裡我把核心的點提了出來,小夥伴自己一定要多去了解和掌握。

高階性

高階性是函數語言程式設計的一個核心思想之一,在更高抽象的實現上,必須依靠高階性來實現。通過把函式作為引數,對當前函式注入解決特定任務的特定行為,來代替充斥著臨時變數與副作用的傳統迴圈結構,從而減少所要維護以及可能出錯的程式碼。

我認為,高階性是一個非常重要的特性,想玩轉函數語言程式設計,就必須要玩轉高階性。為

為什麼我要這樣說呢?

因為你會發現,函式的管道化,組合,柯里化,都是要依靠函式的高階性來實現。擁有高階性的函式,我們稱它是高階函式,那麼什麼樣的函式成為高階函式呢。

目前我理解的高階函式---HOC(Higher-Order-Functions)的定義是這樣的:

傳入的引數是函式,返回的結果是一個函式,這兩個條件,具備一個,就可以稱函式是高階函式。

關於高階函式,更多是去理解和實戰,這裡關於理論方面的知識我就不多介紹了,大家結合我的實戰篇好好理解一下。

其他理論知識

上面我提到的理論知識,都是原子級別的特性,也算是 FP 的基石。像柯里化、偏應用、函式組合、函子等其他更高階的實現,都是在前面這些 基石的基礎上實現的。所以這裡理論篇我就不介紹他們了,小夥伴們自行去分析和學習,我會在實戰篇把這些高階實現串起來。當然如果對這方面理論知識有興趣的小夥伴,想和我交流的,也可以私聊我。

關於 FP 理論知識的總結

FP 是個好東西,它可以幫助你控制程式碼的複雜性,可以增強你的邏輯能力,可以讓測試更容易,等等。

現在,大家對比下面兩個問題:

  1. 學習演算法對前端工程師有用嗎
  2. 學習函數語言程式設計對前端工程師有用嗎

你會發現,都有用,但不代表我們就要在實際前端場景中大量使用它們。

個人認為前端需要掌握 FP 的水平

上面我介紹了我認為目前 FP 領域內,需要掌握的一些元知識,只有掌握了這些元知識,我們才能更好更快的掌握基於元知識衍生出的各種功能實現。比如柯里化函式、偏應用函式、組合函式、高階函式甚至函子等。

但其實,在我實踐了 FP 後,我個人認為,現在的前端工程師在 FP 方面,只需要把組合,柯里化,和高階玩好就行了。其他的高階用法可以先不用進行實踐。我認為前端工程師掌握這三個,就已經能夠在前端把函式式思想發揮到淋漓盡致的地步了。這是在學習 FP 的感悟。

文末總結

本來想寫一篇的,但是發現一篇太長了,就分成了上下兩篇。

函數語言程式設計這塊,涉及到的東西非常多,光理論知識就很多了,我的這篇文章對於理論知識,沒有過多的去解釋和分析,我是按照我個人的理解和總結去向大家展示如何搞定函數語言程式設計的理論知識。所以可能會有一些意見的不統一,或者理論知識涵蓋的不全,還請多多理解和支援。這篇文章不是單純的只分享如何搞定 FP ,是結合如何編寫高質量的函式去向大家進行綜合闡述。

參考

參考連結

參考書籍

  • JavaScript ES6 函數語言程式設計入門經典
  • JavaScript 函數語言程式設計指南
  • Haskell 趣學指南
  • 其他電子書

交流

如何編寫高質量函式系列文章如下(不包含本篇):

可以關注我的掘金部落格或者 github 來獲取後續的系列文章更新通知。掘金系列技術文章彙總如下,覺得不錯的話,點個 star 鼓勵一下。

github.com/godkun/blog

我是原始碼終結者,歡迎技術交流。

如何編寫高質量的函式 -- 打通任督二脈篇[理論卷]

也可以進 前端狂想錄群 大家一起頭腦風暴。有想加的,因為人滿了,可以先加我好友,我來邀請你進群。

如何編寫高質量的函式 -- 打通任督二脈篇[理論卷]

風之語

最後:尊重原創,轉載請註明出處哈?

相關文章