組合軟件:2. 爲什麼要在 JavaScript 中學習函數式編

組合軟件:2. 爲什麼要在 JavaScript 中學習函數式編程?https://www.zcfy.cc/article/why-learn-functional-programming-in-javascript-composing-software

原文鏈接: medium.com

請忘掉你認爲你知道的有關 JavaScript 的任何東西,以初學者心態來接觸這份資料。爲幫助你這樣做,我們打算從頭開始複習 JavaScript 的基礎知識,就好像你以前從來沒有看到過 JavaScript 一樣。如果你是初學者,那就走運了。最後從零開始探索 ES6 和 函數式編程!希望所有新概念在這個過程中都被解釋到了 - 但是別指望會太舒適。

如果你是一個已經熟悉 JavaScript 或者純函數式語言的老手,也許你會認爲用 JavaScript 來探究函數式編程簡直個笑話。請把這些想法放一邊,試着用開放的心態來接觸這份資料。你可能會發現這是另一個層級的 JavaScript 編程。你從未知道它的存在。

既然本文是稱爲“組合軟件”,而函數式編程是組合軟件的顯而易見的方式(使用函數組合、高階函數等等),你可能會想知道爲什麼我不討論 Haskell、ClojureScript 或者 Elm,而是 JavaScript。

JavaScript 有函數式編程所需的最重要的特性:

  1. 一等函數:即將函數用作數據值的能力:傳遞函數爲參數、返回函數以及將函數複製給變量和對象屬性。這個特性允許高階函數,從而能啓用偏應用、柯里化和組合。
  2. 匿名函數和簡潔的 lambda 語法x => x * 2 在 JavaScript 中是有效的函數表達式。簡潔的 Lambda 讓它更容易與高階函數配合。
  3. 閉包:閉包是函數與其詞法環境捆綁在一起。閉包是在函數創建時創建的。當一個函數被定義在另一個函數內部時,它可以訪問外層函數中綁定的變量,即使在外層函數退出之後也是如此。閉包是偏函數應用獲取其固定參數的手段。固定參數是一個綁定在一個被返回的函數的閉包作用域中的參數。在 add2(1)(2) 中,1 就是被 add2(1) 返回的函數中的固定參數。

JavaScript 缺什麼

JavaScript 是一種多範式語言,就是說它支持多種不同風格的編程。JavaScript 支持的風格包括過程式(命令式)編程(像 C 一樣,函數代表可以被反覆重用和組織的指令子程序),面向對象編程(對象,不是函數,是基本構造單元),當然還有函數式編程。多範式語言的缺點是命令式和麪向對象編程趨向於暗示幾乎所有東西都需要是可變的。

變動(Mutation)是指發生在原地的對數據結構的改變。例如:

const foo = {
  bar: 'baz'
};

foo.bar = 'qux'; // mutation

對象通常需要是可修改的,這樣其屬性就可以通過方法來修改。在命令式編程中,大多數數據結構都是可修改的,以保證高效的對象和數組的原地操作。

如下是一些函數式語言有,但是 JavaScript 沒有的特性:

  1. 純度:在有些函數式編程語言中,純度是通過語言強制的。帶有副作用的表達式是被禁止的。
  2. 不可變性:一些函數式語言禁用了變動。表達式被求值爲新的數據結構,而不是修改已有的數據結構,比如數組或者對象。這可能聽起來效率低下,不過很多函數式語言在幕後使用字典樹數據結構,而字典樹這種結構是以結構性共享爲特徵:這意味着舊對象和新對象共享引用相同的數據。
  3. 遞歸:遞歸是函數爲迭代用途引用自身的能力。在很多函數式語言中,遞歸是迭代的唯一方法。沒有像 forwhile 或者 do 循環這樣的循環語句。

純度:在 JavaScript 中,純度必須按約定實現。如果不是通過組合純函數來創建大多數應用程序,就不是用函數式風格編程。很不幸的是,在 JavaScript 中通過偶然創建和使用非純函數很容易偏離軌道。

不可變性:在純函數語言中,不可變性通常是被強制的。JavaScript 缺乏被大多數函數式語言所用的高效、不可變的基於字典樹的數據結構,不過有些庫可以幫助實現,包括 Immutable.jsMori。希望將來的 ECMAScript 版本規範會包含不可變數據結構。

有帶來希望的跡象,比如 ES6 中添加的 const 關鍵字。用 const 定義綁定的名稱不能再次賦值引用不同的值。理解 const 並不代表不可變值很重要。

const 聲明的對象不能被重新賦值來引用一個完全不同的對象,不過它應用的對象可以有可修改的屬性。JavaScript 還能 freeze() 對象,不過這些動向只在根級別上是凍結的,就是說嵌套的對象依然可以有可修改的屬性。換句話說,在 JavaScript 規範中我們要看到真正的組合不可變,依然有很長一段路要走。

遞歸:JavaScript 從技術上講是支持遞歸的,不過大多數函數式語言有一個稱爲尾調用優化的特性。尾調用優化是讓遞歸函數能重用遞歸調用的棧幀。

如果沒有尾調用優化,調用棧就會無休止增長,從而導致棧溢出。JavaScript 在 ES6 規範中從技術上講是有一個有限形式的尾遞歸優化。不幸的是,只有一個主流瀏覽器引擎實現了它,並且優化只是部分實現了,然後後來從 Babel(最熱門的標準 JavaScript 編譯器,用於將 ES6 編譯爲 ES5,以便在較老的瀏覽器中使用)中刪掉了。

結果:對於大的迭代,使用遞歸依然是不安全的 - 即使你很小心在尾位置調用函數。

什麼是 JavaScript 有的,而純函數語言沒有的

語言純正癖者會告訴你,JavaScript 的可變性是其主要缺陷,這是真的。不過,副作用和可變動有時候是有好處的。實際上,創建大多數有用的現代應用程序要想沒有副作用是不可能的。像 Haskell 這種純函數式語言也要用到副作用,只不過是使用稱爲 Monads 的盒子將副作用僞裝成純函數,從而讓程序保持純,即使被 monad 表示的副作用是不純的。

Monad 的麻煩是,即使其用法很簡單,但是對哪些對很多示例不熟悉的人解釋什麼是 Monad,有點像向盲人解釋藍色看起來像什麼一樣。

“一個單子(Monad)說白了不過就是自函子(Endofunctor)範疇上的一個幺半羣(Monoid)而已,這有什麼難以理解的?” ~ James Iry, fictionally quoting Philip Wadler, paraphrasing a real quote by Saunders Mac Lane. “編程語言簡史(僞)”

通常,模仿誇大了的事情會讓一個笑點更搞笑。在上面的引言中,Monad 的解釋實際上是對原始引言做了簡化。原始引言是:

"X 中的一個單子只是 X 的自函子範疇上的一個幺半羣,積 x 被自函子的組合替換,單元集合被 indentity 自函子替換。 "數學工作者必知的範疇學"

即使是這樣,在我看來,害怕 monad 的論證很無力。學習 Monad 的最佳方式不是去讀一堆該主題的書和博文,而是直接開始用它。如同函數式編程中的大多數事情一樣,費解的學術詞彙比概念更難理解。相信我,爲理解函數式編程,你不必理解桑德斯·麥克蘭恩。

雖然 JavaScript 對於每種編程風格可能都不是完全理想,但是它無可爭辯的是一種被設計用來可以被不同編程風格和背景的不同人羣所使用的通用語言。

根據 Brendan Eich 所言,這從一開始就是有意而爲之的。那時,網景必須支持兩類程序員:

“…組件程序員,他們用 C++ 或者(我們希望的)Java 編寫組件;以及業餘或者專業的腳本編寫者,他們編寫的代碼要直接嵌入到 HTML 中。”

最初,網景的意圖是要支持兩種不同的語言,而腳本語言可能會類似於 Scheme(一種 Lisp 的方言)。Brendan Eich 又說:

“我被招募進網景是他們答應我可以在瀏覽器中玩 Scheme。”

然而,JavaScript 必須是一門新語言:

來自高層工程管理人員的命令是:這門語言必須看起來像 Java。這實際上就已經把 Perl、Python、Tcl 以及 Scheme 排除在外。”

所以,從一個開始,Brendan Eich 腦子中的想法就是:

  1. 瀏覽器中的 Scheme。
  2. 看起來像 Java。

它最終就成了一個大雜燴:

“我並非驕傲,只不過是很高興我選擇 Scheme 式的一等函數以及 Self 式(儘管很怪異)的原型作爲主要因素。至於 Java 的影響,主要是把數據分成基本類型和對象類型兩種(比如字符串和 String 對象),以及引入了Y2K 日期問題,這真是不幸。 ”

我把最終進入 JavaScript 中的一些”不幸“類似 Java 的特性加入到如下列表中:

  • 構造器函數和 new 關鍵字,從工廠函數有不同的調用方式和用法語義。
  • class 關鍵字加上單一祖先的 extend 作爲主要的繼承機制。
  • 用戶的偏好是把一個 class 當作是一個靜態類型(實際上它不是)。

我的建議是:儘可能避免這些玩意。

幸運的是,JavaScript 已經結束了成爲這樣一種包羅萬象的語言,因爲事實證明腳本的方式已經完勝“組件”的方式(今天,Java、Flash 和 ActiveX 插件已經不被大多數安裝了的瀏覽器所支持)。

最終只有一種語言直接被瀏覽器所支持:JavaScript。

也就是說瀏覽器不再臃腫而且 bug 更少,因爲它們只需要支持一套語言綁定的設置:JavaScript 的。你可能會想 WebAssembly 是一個例外,不過 WebAssembly 的設計目標之一是用一種兼容的抽象語法樹(AST)來共享 JavaScript 的語言綁定。事實上,將 WebAssembly 編譯爲 JavaScript 的子集的第一次示範就是 ASM.js。

作爲 Web 平臺上唯一的標準通用編程語言的地位,讓 JavaScript 順勢成爲軟件歷史上最流行的語言:

App 贏得了世界,web 贏得了 app,而 JavaScript 贏得了 web。

多種 評測表明, JavaScript 現在是世界上最熱門的編程語言。

JavaScript 並非是函數式編程最理想的工具,不過它一個在大型、分佈式團隊上創建大型應用程序的極佳工具,而不同的團隊對於如何創建一個應用程序會有不同的想法。

有些團隊會專注於腳本粘合,這時命令式編程特別有用。其它一些團隊會專注於創建架構的抽象,這時一點(有節制的,小心謹慎的) OO 的思想可能不是一個壞主意。還有一些其他團隊會擁抱函數式編程,對確定性的、可測試的應用程序狀態管理採用純函數來減少用戶行爲。這些團隊中的成員都是使用同一語言,也就是說他們可以更容易交換想法,相互學習,並且在在彼此的工作基礎上做事。

在 JavaScript 中,所有這些想法都能共存,讓更多人可以擁抱 JavaScript,從而已經導致了世界上最大的開源包註冊庫(到 2017 年 2 月爲止)npm的出現。

JavaScript 的真正威力在於生態系統中思想和用戶的多元化。對於函數式編程語言的純正癖者來說,它也許不是絕對理想的語言;不過,對於來自其它流行語言(比如 Java、Lisp 或者 C)的人來說,要在一起工作,必須要熟悉可以工作在你可以想像的幾乎所有平臺上的一種語言,那麼 JavaScript 可能是理想的語言。JavaScript 對於有這些語言背景的用戶來說,不會感到很舒服,不過他們會對學習該語言並快速提高工作效率感到足夠舒服。

我贊同 JavaScript 不是函數式程序員的最佳語言。不過,沒有任何其它函數式語言可以宣稱它是一種每個人都可以使用和擁抱的語言,正如 ES6 所展示的:JavaScript 能變得更擅長於服務於對函數式編程有需求的用戶。與其拋棄被世界上幾乎所有公司所用的 JavaScript 以及不可思議的生態系統,爲什麼不擁抱它,並且逐步讓它成爲用於軟件組合的一種更好的語言呢?

現在 JavaScript 已經是一種足夠好的函數式編程語言,也就是說人們正在 JavaScript 中用函數式編程技術創建各種有用和有趣的東西。Netflix(以及用 Angular 2 以上創建的每個應用)使用基於 RxJS 的函數式工具。Facebook 在 React 中用了純函數、高階函數和高階組件的概念來創建 Facebook 和 Instagram。PayPal、KhanAcademy 和 Flipkart 用 Redux 來做狀態管理。

它們並不孤獨:Angular、React、Redux 和 Lodash 都是 JavaScript 應用程序生態系統中領先的框架和庫,並且它們都是深受函數式編程的影響 — 或者以 Lodash 和 Redux 爲例,它們構建出來的明確目的就是在真正的 JavaScript 應用程序中啓用函數式編程範式。

爲什麼要用 JavaScript?因爲 JavaScript 是大多數真正的公司正用來創建真正的軟件的語言。不管你愛它,還是恨它,JavaScript 已經把最流行的函數式編程語言的頭銜從旗手 Lisp 那兒偷走了。是的,對於函數式編程概念來說,Haskell 現在是更適合的旗手,不過人們就是沒用 Haskell 創建多少真正的應用程序。

任何時候,在美國有近十萬個,在全球有數十萬個 JavaScript 職位空缺。學習 Haskell 會教你很多有關函數式編程的知識,不過學習 JavaScript 會教你很多創建真實工作中的產品應用的知識。

App 贏得了世界,web 贏得了 app,而 JavaScript 贏得了 web。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章