前端開發js函數式編程真實用途體現在哪裡?

函數式編程教程很多,但不知道其解決了哪些問題。感覺只是寫法上不同而已


想到哪寫到哪,先列幾條:

1. 優化綁定:說白了前端和後端不一樣的關鍵點是後端HTTP較多,前端渲染多,前端真正的剛需是數據綁定機制。後端一次對話,計算好Response發回就完成任務了,所以後端吃了二十年年MVC老本還是挺好用的。前端處理的是連續的時間軸,並非一次對話,像後端那樣賦值簡單傳遞就容易斷檔,導致狀態不一致,帶來大量額外複雜度和Bug。不管是標準FRP還是Mobx這種命令式API的TFRP,內部都是基於函數式設計的。函數式重新發明的Return和分號是要比裸命令式好得多的(前端狀態可以同步,後端線程安全等等,想怎麼封裝就怎麼封裝)。

2. 封裝作用:接上條,大幅簡化非同步,IO,渲染等作用/副作用相關代碼。和很多人想像的不一樣,函數式很擅長處理作用,只是多一層抽象,如果應用稍微複雜一點,這點成本很快就能找回來(Redux Saga是個例子,特別是你寫測試的情況下)。渲染現在大家都可以理解冪等渲染地好處了,其實函數式編程各種作用和狀態也是冪等的,對於複雜應用非常有幫助。

(很多人認為函數式編程沒用,主要是因為寫的東西太簡單,這也是為啥大數據團隊Scala比例這麼高的原因)比如這個經典答案:談談為什麼上scala? - 知乎

3. 復用:引用透明,無副作用,代數設計讓函數式代碼可以正確優雅地復用。前端不像後端業務固定,做好業務分析和DDD就可以搭個靜態結構,高枕無憂了。前端的好代碼一定是活的,每處都可能亂改。可組合性其實很重要。通過高階函數來組合效果和效率都要高於繼承,試著多用ramda,你就可以發現絕大部分東西都能一行寫完,最後給個實參就變成一個UI,來需求改兩筆就變成另外一個。

(好像輪子哥以前還說過其實Monad這種東西反而適合搬磚,我覺得因為API簡單好用,而且它也沒給你額外的能力,讓你做到以前做不到的東西)。

到底什麼是函數式編程?

其實函數式編程是個籠統的稱呼,不太容易一概而論。到底用了map filter compose就叫函數式編程,有一等函數就算,還是依賴hm類型系統的的靜態類型語言,還是依賴Monadic IO,還是要自動並行化才算函數式編程呢,還是都不算,只有Lambda演算才是?這個問題很難回答,比較像局外人的統稱(你看西方人看見長得像中國人經常說Asian),說OO也會有Smalltalk和Simula這兩種調用機制完全不同的流派,也有C++和Ruby靜態動態類型不同的運行機制,更別提更多混合範式的OO語言。它們擅長解決的事情截然不同。對FP來說,也是一樣的:從最標準的Lambda演算到Haskell,再到ML系語言,再到Lisp等等,FP的含義其實是不同的。

所以JavaScript算不算FP語言?

不過一般人們把Scheme也稱做函數式編程語言。JavaScript和Scheme比也就少了元編程,事實上JavaScript本來就是打算仿照Scheme設計的腳本語言,只不過95年Java太火了,因此NetScape想讓語法像Java一些……於是你就看到了現在的JavaScript。

所以按照一般工程上可以接受的說法,JavaScript因為有一等函數這張門票,大部分時候可以算FP語言。(不說JavaScript,Java 8函數式編程的書市面上就有多少本!)

除了類型系統上由於JavaScript是動態類型語言,不容易模擬類型系統之外,大部分函數式特性都可以在JS里比較自然地實現。

JavaScript函數式編程的現狀和未來?

實話說各種編程開發應用函數式編程的其實比較少。實際上能看到函數式架構的項目除了某些銀行內部的高端項目基本就沒見過太多,不然就是大數據處理這些和Web直接關聯比較小的。React Redux帶起這波節奏反而讓函數式編程在前端領域裡佔有相當的一席之地了。

不要小看Redux這麼幾行代碼——基本上就是後端喊了很多年都沒搞起來的CQRS + Event Sourcing的優雅實現,甚至你能看到很罕見的Haskell真實項目基本也都只能用這個架構。前端沒有性能壓力,沒有持久化,沒有向後兼容版本問題,和這種模式天然親和,避免了這種模式的所有缺點,只獲得了好處。

除此之外(State, Action) =&> State這種純函數狀態表示,之前也僅在Haskell和Scala裡面常用到。加上React本身的UI = View(State)這種冪等渲染的模式,基本已經算是相當函數式的架構了。(當然很多組件一大堆方法,內部狀態,老從Promise獲取然後賦值等等的項目不算……對Redux利用率比較高的項目才算)。

再往未來展望的話就是Observable這種流式結構和現在Redux這種Plain函數式風格的較量了。特別像Scala現在到底是用Akka這種純Actor API還是用Akka Stream這種流式API好的趕腳——Redux比較像Actor,Observable就是Stream。


瀉藥。

前端很經常遇到一個問題就是,頁面組件之間共享狀態管理。A 狀態被 B、C 組件影響或者依賴,那麼就會遇到問題,A 狀態會被 B、C 組件修改。一旦這種共享狀態多起來了,那麼整個應用就變得越來越不可預測:我這個組件為什麼會這樣,到底是誰改的數據,看了一下,D、E、F、G 函數會使用這個狀態, H、I、J、K、L 組件會影響這個狀態,debug 起來就極其麻煩。這其實就是老生常談的要避免全局變數的問題。

但是在前端基本滿地都是共享狀態,一個狀態被多個組件依賴或者影響基本是不可避免的問題。那如何規避上述的不可預測的難題?我們來約法三章:誰都不可以修改狀態,問題解決。怎麼做到的?你要修改數據,你得新建數據。

新的數據和老的數據共享相同結構,不同的內容需要新建。

(圖片來源:Understanding Clojureamp;amp;amp;#x27;s Persistent Vector, pt. 1)

於是函數式編程裡面的 immutablepure function 體現了它們在前端開發中的優勢。如果我們寫的組件盡量都是 pure 的,所有盡量函數都是 pure 的,我們就知道它們根本不會有副作用,也就是不會修改狀態(它甚至不會依賴狀態)。那麼 debug 起來就極其方便,我知道 D、E、F、G 函數是 pure,不是它的問題;我知道 H、I、J、K、L 是 pure 組件,不是它的問題。而 M 不是 pure 的,是不是你乾的好事?

這是我看到前端開發js函數式編程真實用途體現:immutablepure function 幫助更好的管理狀態,使得應用的可預測性更強,降低代碼管理難度。即便這樣,前端基本都是和副作用打交道,所以不可能所有東西都是 pure 的,副作用其實不可避免,pure 只能盡量而不要強求。

但是(但是來了),我並沒有看到函數式編程的其它方面有對前端有什麼特別的好處。例如 composition,用 currying 造 one-argument function,然後再 compose 起來,使得一切都是函數,數據就是函數。其實聽起來很美,但實際上可讀性很差,而且團隊的學習成本很高。最後你的代碼會是很多很多精短的函數,然後很多很多的 _.compose _.chain _.curry。熟悉這一套的朋友可能還好,新入職的朋友可能就把控不了了(有些人看到 `() =&> () =&>` 就開始暈頭轉向了,更別說複雜的函數組合);還有強調完全無副作用這個還是非常難辦的,如果用 Monad,那麼整個前端 App 就是 Monad 了,HTTP 是副作用, DOM 是副作用,連 console.log 也是副作用。

所以,這只是個人的愚見,一些實戰中的思考。充分利用函數式編程中的 immutablepure function 更好的管理我們前端應用共享狀態;而其它的方面實話說不是特別合適,好不好用要看什麼特性在什麼場景下解決什麼樣的問題,我並沒有看到 composition 在前端開發帶來什麼好處,解決了什麼場景下的問題。

額外資料:動手實現 Redux(一):優雅地修改共享狀態

拋磚引玉。


針對這個問題,前端小夥伴幾個開了個 repo:FEPatients/with-or-without-fp,裡面記述了使用 FP 和不適用 FP 完成相同的業務邏輯的差別一些例子,並考慮了fantasyland/fantasy-land(但是例子可能舉得不是太好,也就沒能體現FP的優勢,後續會一直完善,如果有感興趣的大手子,也可以幫忙訂正或者提交新的 with-without 範例。):

Without Either,我們這麼做,沒了try-catch,沒了if-else:這樣做:

const players = require("../players");

const getLastNames = players =&> {
try {
const names = players.map(player =&> player.name);
const lastNames = names.map(name =&> name.split(" ")[1]);
return lastNames;
} catch (e) {
console.error("error:", " players should be an array")
}
}

console.log("players:", getLastNames(players)); // =&> ["Curry", "James", "Paul", "Thompson", "Wade" ]
getLastNames({}); // =&> error: players should be an array

With Either,我們這麼做,沒了try-catch,沒了if-else:

const players = require("../players");

const getLastNames = players =&> {
try {
const names = players.map(player =&> player.name);
const lastNames = names.map(name =&> name.split(" ")[1]);
return lastNames;
} catch (e) {
console.error("error:", " players should be an array")
}
}

console.log("players:", getLastNames(players)); // =&> ["Curry", "James", "Paul", "Thompson", "Wade" ]
getLastNames({}); // =&> error: players should be an arrayconst F = require("../../fp");
const players = require("../players");

const getPlayers = players =&> !Array.isArray(players) ?
F.Left.of("players should be an array") :
F.Right.of(players);

const getLastNames = F.compose(
F.feither(F.log("error", "error:"), F.log("debug", "players:")),
F.fmap(F.map(F.last)),
F.fmap(F.map(F.split(" "))),
F.fmap(F.map(F.property("name"))),
getPlayers
);

getLastNames(players); // =&> players: [ "Curry", "James", "Paul", "Thompson", "Wade" ]
getLastNames({}); // =&> error: players should be an array

初看下來,可能一頭霧水,這就是 FP 很大一個問題,入門稍難,學習曲線陡峭,畢竟從指令式編程到函數式編程不只帶來編碼風格的改變,還有思維習慣的轉變。當時幾個小夥伴啃 MostlyAdequate/mostly-adequate-guide (有別於其他只是介紹了高階函數和map、reduce就自稱是 js fp 教學的文章,該教程十分深入)也啃得費力,啃完興奮的不行,感覺一片新天地。

Eric Elliott (強烈的)在他的系列文章 Composing Software 中認為,FP 帶來的軟體編寫之道就在於函數組合,不再需要 if-else, for-while, switch-case,try-catch 等控制語句,不再需要複雜的面向對象,實際項目中,似乎只要一個 ramdajs 打底,就足夠 compose 出我們所需要的任何業務邏輯。但目前我也不知道在大型工程裡面 compose 出來的,遍地開花的新函數會不會難於維護和閱讀。如果還要滿足 fantasy-land,那麼引入的各個 Functor 以後也甩不掉了,統統按照規範來,幾乎沒有退路。

現在團隊開始在新項目用 ramda 替掉 lodash,一些可以試錯的 demo 甚至考慮引入 Functor,之後看看效果再回來修改問題,現在,我們預見了一些好處,比如 pure function 、immutable 帶來的穩定,curry,compose,pointfree 帶來的清晰的業務梳理。

我學習函數式的初衷很簡單,作為入門級碼農,不管函數式 js 利弊如何,react,redux,rxjs,cyclejs 這些熱門庫都或多或少的吸納了函數式編程的元素,想往上爬,想讀懂這些框架或者庫的源碼和設計思路嗎,或者說想稍微趕上矽谷程序員的節奏,函數式編程都是不得不學的。推薦幾個函數式的文章或者會議演講,大多數都描述了使用函數式和不使用函數式間的差別:

    MostlyAdequate/mostly-adequate-guide

  1. Composing Software 系列:https://medium.com/javascript-scene/reduce-composing-software-fe22f0c39a1d
  2. Hey Underscore, You"re Doing It Wrong!: https://www.youtube.com/watch?v=m3svKOdZijA
  3. funfunfunction: https://www.youtube.com/channel/UCO1cgjhGzsSYb1rsB4bFe4Q
  4. Functional Programming is Terrible:https://www.youtube.com/watch?v=hzf3hTUKk8U


參考 Wiki Functional programming, FP 涉及的概念很多, 然而真正在前端得到了廣泛應用的僅僅是 first class and higher order functions, 而且這一點再相當多編程語言當中都被使用, 在前端特別能用回調函數的使用和 lodash 之類函數庫當中看到.

其他的概念, 比如純函數和引用透明, 在 React 當中得到了認可和使用, pure render 藉助的是純函數概念, Immutable.js 依靠的就是引用透明這個特性.

遞歸, 惰性求值, 代數類型系統, 數據結構, 這些手法在前端的使用較少, 或者因為要在語言層面有優化, 而 js 目前並沒有良好的支持. 比如 TCO 還沒有被主流瀏覽器實現, 比如 TypeScript 只是部分借鑒了 FP 語言當中的代數類型系統和類型推導.

JavaScript 並不是函數式編程語言, 只是兼容了一部分 FP 方法風格的使用技巧. 前端更多是混合範式用來解決具體的問題. 真正需要大量踐行 FP 概念的, 恐怕是只有 React 之類的少數場景而已. 畢竟沒有 FP 依然能把問題解決掉, 很多人不會喜歡 FP 在思維上的轉化.

在前端之外我看到的 FP 的適用場景, 一個是 Erlang 那樣的系統可以解決 FP 的做法提供強大的並發(這條有疑點, 見評論), 一個是 OCaml 通過強大的類型系統經常用在開發編程語言相關的 prototype 上(比如 Flow, WebAssembly).


很多人有個誤解,以為這樣寫就是函數式,那樣寫就不是了.

其實我想說,函數式不僅能定性分析,也是可以定量分析的.

提高代碼質量系列之三:我是怎麼設計函數的? - 碎景 - 博客園


看看redux的源碼 裡面reduce、 compose的地方寫的不要太美


我們前端總是要比整個業界慢一拍哈,函數式已經是業界發展很成熟的東西,這幾年前端的函數式編程才熱起來。

在JS中的函數式編程好處,和業界的函數式變成好處是一樣的。

組合性(Composibility),一個函數只有一個功能,多個函數組合起來就有很強大的功能。不過,個人覺得玩函數組合的場合還是比較少,類似 foo = compose(f,g,h) 這樣的組合,框架本身可能用得到,應用層面用得場合少。

數據不可變(Immutability),也就是數據不能被改變,這個最有用,程序容易debug,或者說更不容易出bug。對於面向對象的思維來說,數據被封在對象中,所有的修改的歷史也就被隱藏了;對於函數式變成來說,數據都是不能改變的,所以整個歷史都是有跡可循的。

純函數(Pure Function),函數的輸出只和輸入參數有關係,這樣程序的結果是可預期的,這樣既利於單元測試,也利於減少bug。

先寫這麼多。


1. 函數式編程相對過程式,有很多優點也有很多缺點,肯定有人愛有人恨

2. JavaScript單純從語言的角度來看,可以認為能夠進行比較純粹的函數式編程,但是硬傷太多,別這麼用

3. 吸收一些函數式編程思想是好的

4. 你在網上看到的絕大多數自稱函數式編程的,尤其是把map和filter當函數式的,都是瞎掰。map和filter的定性應該是:函數式編程語言最先設計出來很好用的數組操作API,被遷移到了JavaScript的OO風格庫上。

5. 識別一個人是不是函數式編程小白的最簡單方法,就是看他能不能寫出來y-combinator,簡稱yc,在函數式編程中,遞歸是替代循環的最常見手段,而純粹的函數式是沒有直接用函數名遞歸這個設施的,yc正是實現遞歸的一種通用手段。所以,yc在函數式編程中的地位大致相當於循環。

幾個簡單的例子:

變數-過程式

var a = 1;
var b = 2;
a + b;

變數-函數式

((a , b) =&> a + b)(1, 2)

循環-過程式

var sum = 0;
for(var i = 0; i &< 10; i++) sum += i;

循環-函數式-允許使用scheme風格聲明

const f = n =&> n &< 0 ? 0 : n + f(n - 1); f(100)

循環-函數式-純

((y) =&>
(f =&> f(100))
(y(self =&>
n =&> n &< 0 ? 0 : n + self(n-1)))) (g =&>
(f=&>f(f))(
self =&>
g( (...args)=&>self(self)(...args) )
))

我勸過你們了


編程的宗派 —— 王垠

要說開地圖炮,我肯定是比不過王垠的。一篇文章得罪在場所有人 XD


其實react就是函數式思想吧,數據是不可變的,react相當於純函數,展示出來的 ui 就是結果。

redux 更是函數式思想


你說的很可能是高階函數而不是函數式編程


如果沒有first-class function,那麼事件驅動的回調機制就很麻煩,不能直接.addEventListener("click", fn),而需要將fn包裝成對象(像Java)那樣,class FnHandler { public function run () {...} },或者傳指針,像C那樣,*fn,有了first-class function,函數就和其他數據一樣,可以自由地傳來傳去。

如果沒有higher-order function,那麼有些元編程的狀況實現就很麻煩,function processFactory() { return fn () {} },有了高階函數,函數可以返回函數,邏輯可以生成邏輯,就為元編程提供了基礎。


在初次接觸函數式編程時,我以為函數式和面向對象只是「看起來不一樣」,是給人一種更抽象的方式組織自己的程序,但後來我發現這兩者不僅僅是外表不一樣。舉個例子,函數式的很重要一個特點就是狀態不可變,而面向對象往往是擁抱變化的,設計模式里也有state模式,這兩種編程範式就完全是兩個南轅北轍的東西了。而狀態不可變有什麼意義呢,在並發編程中,不可變消除了競態條件,函數式的這些特徵可以拋開線程和鎖的模型,編寫函數式的並發的程序。這一點你可以看看《七天七並發模型》這本書。

我對js不是很熟,不知道js對函數式的支持怎麼樣,我對python熟,在python里提供了很多函數式的方式,但python並不是函數式的語言。所以很多函數式的方法流於表面,使得使用者覺得函數式也就看起來那麼回事。估計js這種同樣起源於非函數式的設計,但在後期加上函數式的語法的語言也是同樣的情況。


簡單來說

跳出前端,整個業界函數式編程火的最最最主要原因是函數式編程模型使得並行編程變得容易,甚至於寫完代碼後代碼就可以做到「自動並行化」,而這一切的好處都來自於不可變數據,對比傳統的編程範式,由於賦值語句的存在,在並行化問題上,你要考慮許多的共享內存和鎖的問題。現如今,在多核系統的普及下,每個人都想壓榨處理器,可對於傳統編程模型,並行化編程又比較困難,函數式編程自然就火了。

回到前端,對於前端來說,引發大家瘋狂學習函數式編程的是react+redux框架,首先,react的設計哲學就是把View=f(data),視圖是數據的函數,數據發生變化時,react的做法是View1=f(data1),View2=f(data2)然後對View1和View2進行diff 找到變化的內容,對DOM進行修改。

有沒有發現,從data1到產生data2的過程就是函數式編程的不可變數據。而整個react的設計哲學就是符合函數式編程的。

再來看redux,redux中很重要的部分是reducer,reducer依賴輸入,通過純函數,產生輸出,可以說是貫徹react的核心思想的。

而對於前端而言,函數式編程可以帶來什麼。

簡單來說,有利於數據狀態的觀察,單元測試和debug。想想redux你就知道了,dispatch一個action,產生全新的state,你就非常容易的知道數據發生了哪些變化,面對前端現如今單頁應用的複雜狀態的情景下這一點還是非常重要的。

當然,函數式編程也有不好的,最重要的就是不可變數據的時間浪費,每次操作都要創建全新的數據,在數據龐大的時候必然會有性能影響,所以要控制好redux中放的狀態數量,當然了,immutableJS帶來的不可變數據結構是一個不錯的解決方案,但JS的對象本身並不immutable的,強行引入第三方庫,其實對於編程體驗又不是一個很好的事情。

所以,本質上react和vue或者說redux和mobx在核心思想上的差別,其實就是不可變數據和可變數據的差異,也就是函數式編程和以賦值為主的傳統編程的對比。

之前看到一句話,「共享的可變數據是萬惡之源」。其實,也正是對共享數據的看法,才有了對redux和mobx或者說react和vue的探討。

另外,ES2017的一個共享內存和原子操作的提案現在已經到了stage4,作為一個前端開發者,我們是不是可以展望一下未來前端或者node的並行編程呢?

(啊啊啊,居然用手機打了這麼多字。每次都把data打成dota,網癮少年 )


對前端而言,最有用的三個概念:

純函數、不可變數據、高階函數。


可以看下Cycle.js的文檔,這個框架同時運用了函數式和響應式的編程思想。當然響應式編程也是基於函數式編程的:ReactiveX is a combination of the best ideas from the Observer pattern, the Iterator pattern, and functional programming.


用過promise處理非同步問題吧,好用吧,那就是一種函數式編程風格


有些代碼改用函數式以後,代碼量大大減少,可讀性大大提高。


那些大概都是「函數式風格的API」吧……至少我現在還是想不通狀態機到底哪裡函數式了……


函數式編程你可以問問做大數據和深度學習的同志用Python爽不爽。


推薦閱讀:

怎麼評價Vue.js2.5以後要大力加強對TypeScript和VSCode的支持?
font-weight和fontWeight的區別?
onclick = xxx這種賦值寫法綁定事件的原理是什麼?
有哪些常用軟體是用WEB前端技術寫的?底層使用瀏覽器殼?
我可以只用flex布局嗎?

TAG:Web開發 | 前端開發 | JavaScript | 前端工程師 | 函數式編程 |