反對函數式編程的政治正確

在技術社區裏,與函數式編程相關的話題一直十分火熱,這尤以素有娛樂圈之稱的前端社區爲甚。大量相關的入門文章中,面向對象與命令式編程經常被做爲對比的反例,似乎它們已是醜陋而骯髒的過期技術了。對這種矯枉過正觀點的擔心,正是這篇文章寫做的初心。前端

爲何這裏會牽扯到政治正確呢?這是由於對編程範式的執着,已經或多或少地成爲了一種道德綁架了:這很接近政治正確的背後,那種強大的道德信念。這種信念摻雜在了對編程語言的信仰之中,其結果是用非強迫的方式完成了對話語權的控制。好比下面這樣的觀點:git

  • 你這段代碼用了 for 循環,這是過程式的。爲了優雅,你應該寫成函數式的。
  • 你這段代碼有反作用,這是骯髒的。爲了純淨性,你應該把 IO 包在 Monad 裏。
  • 你這段代碼用了 class,這是面向對象的。爲了無狀態,你應該寫成高階函數。

這種粗暴的邏輯和【父母都是爲你好】與【女性不適合編程】有什麼區別嗎?許多這樣不負責任的偏激言辭,造就了當前社區中對於面向對象與命令式編程的 Stereotype 刻板印象。實際上,把函數式編程與面向對象 / 命令式編程對立的觀點,其自己在分類上就是不嚴謹的。姑且不論這點,這些論調也至關武斷,忽略了技術的適用場景。github

例如,對於函數式編程的最主要的讚譽之一,就在於純函數的無狀態性質。關於它的好處咱們已經聽到太多了:結果可預期、利於測試、利於複用、利於併發……一切聽起來都這麼理想,因此咱們可以自底到上地用純函數來編寫出一個完整的系統了嗎?這裏的荒誕之處在於,當你越接近一臺計算機 under the hood 的原貌時,你離純函數與無狀態越遠算法

當你想要嘗試理解 CPU / 顯卡 / 網絡等真正的基礎的時候,你會發現它們一點兒也不函數式:編程

  • CPU 自己就是個最典型的狀態機。例如筆者的週末玩具 CHIP-8 模擬器 裏,它的 CPU 狀態就是用幾個變量模擬出的一堆寄存器、堆棧指針和計數器罷了。每條指令都是個修改全局狀態的函數罷了——這固然很不純粹,但十分符合對 CPU 運行方式的直覺與抽象。
  • 驅動顯卡工做的 API 有狀態得使人髮指。相信任未嘗試過從頭搭建 WebGL 渲染管線的同窗都可以明白這是什麼意思。而且在下一代以壓榨出極致性能到導向的顯示驅動 API 裏,須要人肉維護的狀態還會更瑣碎。
  • 網絡協議棧的狀態機如何遷移,已經在《計算機網絡》的課本里畫出了無數次了。即使是已經封裝到應用層的最傻瓜的 Web 頁面裏,看看控制檯裏執行一句 performance.timing 的內容,要想正確地畫出這些字段間的狀態遷移關係都已經絕非易事了。

當你想要理解上面的這些玩意如何工做的時候,你所能查到的最經典的資料與業界最通過實戰檢驗的實現,幾乎都是清一色地在過程式、命令式的編程範式下很是地【有狀態】的。難道說編寫這些基礎性工程壯舉的開發者們,其技術水平都不如函數式編程的佈道師們嗎?設計模式

這個矛盾能夠這樣歸結:函數式編程的理念,更接近理論上的數學概念。而命令式、過程式的編程,更貼近實際工程中硬件的工做方式。兩者的簡潔性是體如今不一樣維度的。做爲例子,許多函數式編程狂熱者所不屑乃至唾棄的 C 風格 while 循環,實際上是個很是易於實現與硬件優化的設計。只要你嘗試過閱讀 gcc 生成的彙編碼,不難發現一個 while 很是容易與彙編的跳轉指令聯繫起來:數組

JMP LOOP   ; 首先跳到底部以開始循環
BEGIN: NOP        ; 空指令佔位符
                  ; ...此處開始放置循環體中代碼
                  ; ...
                  ; ...
                  ; ...執行完循環體內代碼
LOOP:  CMP ...    ; 檢查條件
       JNE BEGIN  ; 若不知足則跳轉到 BEGIN 位置
複製代碼

而對於飽受函數式愛好者們抨擊的 for 循環,實現時只需在上面的控制流裏增長初始化過程與計數器臨時變量就行。而被嫌棄的 break 與 continue 等語法也只須要增長更多的標號便可靈活地基於 JMP 實現。相比於函數調用並返回時所需當心翼翼地保存並恢復上下文的一系列跳轉邏輯,命令式的循環語法顯然更易於實現——固然了,你能夠硬槓說 Scheme 這樣的函數式語言,其解釋器更容易實現,筆者也確實做爲週末玩具而實現過一門 Scheme 方言 哦語言 的解釋器。可是,這種幾乎等於手寫語法樹的語言有多少工程中的實用價值呢?相似的地方還體如今對各類數學計算的抽象上。如筆者蹭 PR 貢獻過的 gl-matrix 矩陣運算庫,其中大量的 mutation 也很是不函數式,但它實際上仍然十分簡潔可靠而高效呀。網絡

有些函數式編程的愛好者們,對遞歸一類函數式手法的 all in 推崇也有些使人費解:你寫的什麼 for 循環過低端啦,看我寫成優雅的尾遞歸還自帶解釋器優化不會爆棧呢!誠然,在處理嵌套的數據結構的時候,使用遞歸是至關簡潔易讀的。這一點筆者在畢業前嘗試實現遞歸降低和 LALR 語法分析器的課後做業時,就有了這樣的體會:這時非遞歸的寫法實在很囉嗦。然而,這種原本是以必定的性能代價來提升可讀性的手法,卻經常被誤解爲優秀的實現而被濫用。例如,一個深拷貝算法用遞歸實現當然簡單,但它客觀地存在 Stack Overflow 的風險。做爲解決方案,咱們能夠用數組模擬棧來實現它。這時你的代碼就沒有那麼函數式了:你會由於函數式的代碼更加優雅,就拒絕修復棧溢出的 bug 嗎?數據結構

相似的捨近求遠,還體如今對一些實際上更加晦澀的概念的推崇上。許多但願入門函數式編程的初學者,都會被 Monad 這個【自函子上的幺半羣】概念唬住。誠然,你能夠把 Hooks 和 Promise 這些簡單易用的概念解釋成爲 Monad,筆者在入門時也確實寫了一篇文章從單位元與結合律的角度出發,來證實 爲何 Promise 是一種 Monad。然而,明明是實際開發中很是實用且易於理解的東西,卻要使用更難以懂的一套概念去形式化地定義和解釋,這恐怕並不利於優秀工具和理念的普及。好比,日語裏函數的概念叫作関數,不懂日語體系裏的這個詞,也並不影響一個漢語使用者知識體系的自洽以及對函數的使用呀。而且,Monad 其實已經至關於函數式編程範式中的一種【設計模式】了,而對設計模式的摒棄,不正是函數式編程自身的優點之一嗎?架構

提到了設計模式,就繞不開軟件工程。這時候咱們有很多【道】層面的設計準則,但這些真正利於工程 Scale Up 的準則,反倒和具體的編程範式關係不大了。好比,咱們都知道高內聚低耦合的模塊劃分是利於維護的,但在這個維度上起最重要做用的並非與函數式相關的語法特性,而是語言的包管理器與模塊加載規範等。再好比變動遊戲業界的 ECS 架構,它在【組合優於繼承】方向上的演進也仍然是在面嚮對象語言上就可以實現的。即使到了實際的工程案例上,對於前端這種與 UI 深度相關的領域,其中最大規模且最可靠的實現仍然是很是面向對象的——Windows 和 macOS 的桌面 UI 環境並不是源於函數式語言,難道操做系統的桌面管理器會像 Redux 那樣在單個 store 裏管理全局狀態嗎?

行文至此,這篇文章的吐槽應該告一段落了。但咱們顯然並不該該爲了發泄而寫做,筆者更不是命令式編程的死忠粉(相反地,筆者還特意寫過一篇 RxJS 模擬電梯調度 的安利文章)。差很少時候作一些澄清與提出訴求了:

  • 函數式編程很是重要,且在許多細分領域值得學習與推廣。但不能一律而論地認爲它優於面向對象或過程式等其它範式。
  • 函數式編程一樣存在着自身固有的缺陷,這些地方要客觀地看待。
  • 咱們但願可以平等地看待各類編程範式,保持開放的心態,拒絕譁衆取寵的引戰言論,根據實際需求折衷選擇更具開發效率與運行效率的技術方案。

在實際的編碼中,筆者更關注【符合直覺】這一點。這大概包括兩個維度:

  • 一個語言特性的使用方式,是否符合它設計出來要解決的場景。
  • 一個具體需求的實現方式,是否符合對其最爲簡單直接的抽象。

無論黑貓白貓,只要能抓到老鼠就是好貓。只要是可讀可維護的高質量代碼,爲何要在意它屬於哪一個範式呢 :)

相關文章
相關標籤/搜索