[譯] JavaScript 的函數語言程式設計是一種反模式

磊仔發表於2019-02-28

其實 Clojure 更簡單些

寫了幾個月 Clojure 之後我再次開始寫 JavaScript。就在我試著寫一些很普通的東西的時候,我總會想下面這些問題:

“這是 ImmutableJS 變數還是 JavaScript 變數?”

“我如何 map 一個物件並且返回一個物件?”

“如果它是不可變的,要麼使用 <這種語法> 的 <這個函式>,否則使用 <不同的語法和完全不同行為> 的 <同一個函式的另一個版本>”

“一個 React 元件的 state 可以是一個不可變的 Map 嗎?”

“引入 lodash 了嗎?”

fromJS 然後 <寫程式碼> 然後 .toJS()?”

這些問題似乎沒什麼必要。但我猜想我已經思考這些問題上百萬次了只是沒有注意到,因為這些都是我知道的。

當使用 React、Redux、ImmutableJS、lodash、和像 lodash/fp、ramda 這樣的函數語言程式設計庫的任意組合寫 JavaScript 的時候,我覺得沒什麼方法能避免這種思考。

我需要一直把下面這些事記在腦海裡:

  • lodash 的 API、Immutable 的 API、lodash/fp 的 API、ramda 的 API、還有原生 JS 的 API 或一些組合的 API
  • 處理 JavaScript 資料結構的可變程式設計技術
  • 處理 Immutable 資料結構的不可變程式設計技術
  • 使用 Redux 或 React 時,可變的 JavaScript 資料結構的不可變程式設計

就算我能夠記住這些東西,我依然會遇到上面那一堆問題。不可變資料、可變資料和某些情況下不能改變的可變資料。一些常用函式的簽名和返回值也是這樣,幾乎每一行程式碼都有不同的情況要考慮。我覺得在 JavaScript 中使用函數語言程式設計技術很棘手。

按照慣例像 Redux 和 React 這種庫需要不可變性。所以即使我不使用 ImmutableJS,我也得記得“這個地方不能改變”。在 JavaScript 中不可變的轉換比它本身的使用更難。我感覺這門語言給我前進的道路下了一路坑。此外,JavaScript 沒有像 Object.map 這樣的基本函式。所以像上個月 4300 多萬人一樣,我使用 lodash,它提供大量 JavaScript 自身沒有的函式。不過它的 API 也不是友好支援不可變的。一些函式返回新的數值,而另一些會更改已經存在的資料。再次強調,花時間來區分它們是很不划算的。事實大概如此,想要處理 JavaScript,我需要了解 lodash、它的函式名稱、它的簽名、它的返回值。更糟糕的是,它的“collection 在先, arguments 在後”的方式對函數語言程式設計來說也並不理想。

如果我使用 ramda 或者 lodash/fp 會好一些,可以很容易地組合函式並且寫出清晰整潔的程式碼。但是它不能和 Immutable 資料結構一起使用。我可能還是要寫一些引數集合在後而其他時候在前的程式碼。我必須知道更多的函式名、簽名、返回值,並引入更多的基本函式。

當我單獨使用 ImmutableJS,一些事變得容易些了。Map.set 返回全新的值。一切都返回全新的值!這就是我想要的。不幸的是,ImmutableJS 也有一些糾結的事情。我不可避免地要處理兩套不同的資料結構。所以我不得不清楚 x 是 Immutable 的還是 JavaScript 的。通過學習其 API 和整體思維方式,我可以使用 Immutable 在 2 秒內知道如何解決問題。當我使用原生 JS 時,我必須跳過該解決方案,用另一種方式來解決問題。就像 ramda 和 lodash 一樣,有大量的函式需要我瞭解 —— 它們返回什麼、它們的簽名、它們的名稱。我也需要把我所知的所有函式分成兩類:一類用於 Immutable 的,另一類用於其它。這往往也會影響我解決問題的方式。我有時會不自主地想到柯里化和組合函式的解決方案。但不能和 ImmutableJS 一起使用。所以我跳過這個解決方案,想想其他的。

當我全部想清楚以後,我才能嘗試寫一些程式碼。然後我轉移到另一個檔案,做一遍同樣的事情。

JavaScript 中的函數語言程式設計。

反模式的視覺化。

我已孤立無援,並且把 JavaScript 的函數語言程式設計稱為一種反模式。這是一條迷人之路卻將我引入迷宮。它似乎解決了一些問題,最終卻創造了更多的問題。重點是這些問題似乎沒有更高層次的解決方案能避免我一次有又一次地處理問題。

這件事的長期成本是什麼?

我沒有確切的數字,但我敢說如果不必去想“在這裡我可以用什麼函式?”和“我可否改變這個變數”這樣的問題,我可以更高效地開發。這些問題對我想要解決的問題或者我想要增加的功能沒有任何意義。它們是語言本身造成的。我能想到避免這個問題的唯一辦法就是在路的起點就不要走下去 —— 不要使用 ImmutableJS 、ImmutableJS 資料結構、Redux/React 概念中的不可變資料,以及 ramda 表示式和 lodash。總之就是寫 JavaScript 不要使用函數語言程式設計技術,它看似不是什麼好的解決方案。

如果你確定並同意我所說的(如果不同意,也很好),那麼我認為值得花 5 分鐘或一天甚至一週時間來考慮:保持在 JavaScript 路子上相比用一個不同的東西取代,耗費的長期成本是什麼?

這個所謂不同的東西對於我來說就是 Clojurescript。它是一門像 ES6 一樣的 “compile-to-JS” 語言。大體上說,它是一種使用不同語法的 JavaScript。它的底層是被設計成用於函數語言程式設計的語言,操作不可變的資料結構。對我來說,它比 JavaScript 更容易,更有前途。

Clojure/Clojurescript 是什麼?

Clojurescript 類似 Clojure,除了它的宿主語言是 JavaScript 而不是 Java。它們的語法完全相同:如果你學 Clojurescript,其實你就在學 Clojure。這意味著如果你瞭解了 Clojurescript,你就可以寫 JavaScript 和 Java。“30 億的裝置上執行著 Java”;我非常確定其他裝置上執行著 JavaScript。

和 JavaScript 一樣,Clojure 和 Clojurescript 也是動態型別的。你可以 100% 地使用 Clojurescript 語言用 Node 寫服務端的全棧應用。與單獨編譯成 JavaScript 的語言不同,你也可以選擇寫一個基於 Java 的 servrer 來支援多執行緒。

作為一個普通的 JavaScript/Node 開發者,學習這門語言及其生態系統對我來說並不困難。

是什麼使得 Clojurescript 更簡單?

在編輯器中執行任意你想要執行的程式碼。

  1. 你可以在編輯器中一鍵執行任何程式碼。 的確如此,你可以在編輯器中輸入任何你想寫的程式碼,選中它(或者把游標放在上面)然後執行並檢視結果。你可以定義函式,然後用你想用的引數呼叫它。你可以在應用執行的時候做這些事。所以,如果你不知道一些東西如何運作,你可以在你的編輯器的 REPL 裡求值,看看會發生什麼。
  2. 函式可以作用於陣列和物件。 Map、reduce、filter 等對陣列和物件的作用都相同。設計就是如此。我們毋須再糾結於 map 對陣列和物件作用的不同之處。
  3. 不可變的資料結構。 所有 Clojurescript 資料結構都是不可變的。因此你再也不必糾結一些東西是否可變了。你也不需要切換程式設計正規化,從可變到不可變。你完全在不可變資料結構的領地上。
  4. 一些基本函式是語言本身包含的。 像 map、filter、reduce、compose 和很多其他函式都是核心語言的一部分,不需要外界引入。因此你的腦子裡不必記著 4 種不同版本的“map”了(Array.map、lodash.map、ramda.map、Immutable.map)。你只需要知道一個。
  5. 它很簡潔。 相對於其他任何程式語言,它只需要短短几行的程式碼就能表達你的想法。(通常少得多)
  6. 函數語言程式設計。 Clojurescript 是一門徹底的函數語言程式設計語言 —— 支援隱式返回宣告、函式是一等公民、lambda 表示式等等。
  7. 使用 JavaScript 中所需的任何內容。 你可以使用 JavaScript 的一切以及它的生態系統,從 console.log 到 npm 庫都可以。
  8. 效能。 Clojurescript 使用 Google Closure 編譯器來優化輸出的 JavaScript。Bundle 體積小到極致。用於生產的打包過程不需要從設定優化到 :advanced 的複雜配置。
  9. 可讀的庫程式碼。 有時候瞭解“這個庫的功能是幹嘛的?”很有用。當我使用 JavaScript 中的“跳轉到定義處”,我通常都會看到被壓縮或錯位的原始碼。Clojure 和 Clojurescript 的庫都直接被顯示成寫出來的樣子,因此不需離開你的編輯器去看一些東西如何工作就很簡單,因為你可以直接閱讀原始碼。
  10. 是一種 LISP 方言。 很難列舉出這方面的好處,因為太多了。我喜歡的一點是它的公式化,(有這麼一種模式可以依靠)程式碼是用語言的資料結構來表達的。(這使得超程式設計很容易)。Clojure 不同於 LISP 因為它並不是 100% 的 ()。它的程式碼和資料結構中可以使用 []{},就像大多數程式語言那樣。
  11. 超程式設計。 Clojurescript 允許你編寫生成程式碼的程式碼。這一點有我不想掩蓋的巨大內涵。其中之一是你可以高效地擴充套件語言本身。這是一個出自 Clojure for the Brave and True 的例子:
(defmacro infix
  [infixed]
  (list (second infixed) (first infixed) (last infixed)))
(infix (1 + 1))
=> 2
(macroexpand `(infix (1 + 1)))
=> (+ 1 1)
; 這個巨集把它傳入 Clojure,Clojure 可以正確執行,因為是 Clojure 的原生語法。複製程式碼

為什麼它並不流行?

既然說它這麼棒,可它怎麼不上天呢?有人指出它已經很流行了,它只是不如 lodash、React、Redux 等等那麼流行而已。但既然它更好,不應該和它們一樣流行嗎?為什麼偏愛函數語言程式設計、不可變性和 React 的 JS 開發者還沒有遷移到 Clojurescript?

因為缺少工作機會嗎? Clojure 可以編譯成 JavaScript 和 Java。它實際上也可以編譯成 C#。因此大量的 JavaScript 工作都可以當作 Clojurescript 工作。它是一種函式式語言,用於為所有編譯目標完成所有的任務。先不論它的價值如何體現,2017 StackOverflow 的調查表明 Clojure 開發者的薪資水平是所有語言中全球平均最高的

因為 JS 開發者很懶嗎? 並不是。正如我在上面所展示的,我們做了大量的工作。有個詞叫 JavaScript 疲勞,你可能已經聽說過了。

我們很抗拒,不想學點新東西嗎? 並不是。 我們已經因採用新技術而臭名昭著。

因為缺乏熟悉的框架和工具嗎? 這感覺上可能是個原因,但 Javascript 中有的東西, Clojurescript 都有與之對應的: re-frame 對應 Redux、reagent 對應 React、figwheel 對應 Webpack/熱載入、leiningen 對應 yarn/npm、Clojurescript 對應 Underscore/Lodash。

是因為括號的問題使得這門語言太難寫了嗎? 這方面也許談的還不夠多,但我們不必自己來區分圓括號方括號 。基本上,Parinfer 使得 Clojure 成為了空格語言。

因為在工作中很難使用? 可能是吧。它是一種新技術,就像 React 和 Redux 曾經那樣,在某些時候也是很難推廣的。即使也沒什麼技術限制 ——  Clojurescript 整合到現有程式碼庫和整合 React 的方式是類似的。你可以把 Clojurescript 加入到已經存在的程式碼庫中,每次重寫一個檔案的舊程式碼,新程式碼依然可以和未更改的舊程式碼互動。

沒有足夠受歡迎? 很不幸,我想這就是它的原因。我使用 JavaScript 一部分原因就是它擁有龐大的社群。Clojurescript 太小眾了。我使用 React 的部分原因是它是由 Facebook 維護的。而 Clojure 的維護者是花大量時間思考的留著長髮的傢伙

有數量上的劣勢,我認了。但“人多勢眾”否決了所有其他可能的因素。

假設有一條路通向 100 美元,它很不受歡迎,而另一條路通向 10 美元,它極其受歡迎,我會選擇受歡迎的那條路嗎?

恩,也許會的吧!那裡有成功的先例。它一定比另一條路安全,因為更多的人選擇了它。他們一定不會遇到什麼可怕的事。而另一條路聽起來美好,但我確定那一定是個陷阱。如果它像看起來那麼美好,那麼它就是最受歡迎的那條路了。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃

相關文章