有哪些函數式編程在前端的實踐經驗?

有哪些函數式編程在前端的實踐經驗?


微軟的 https://github.com/Reactive-Extensions/RxJS


用個FP語言編譯到JS寫界面也叫FP在前端的實踐, Promise是個Monad也叫FP在前端的實踐, 模仿個很多FP語言有的feature也叫FP在前端的實踐, FRP 雖然頭頂個Functional 但是原始FRP是有形式語義定義的, F不能單獨剝離出來討論…再說所謂的實踐藍到不是碰到什麼問題然後用工具去解決它木? 但是沒有見到這種答案, react能算上, 但是又沒人仔細說 :(

BTW, 有人研究過這個么


  1. swannodette/mori · GitHub -- 從 clojurescript 移植到 js 的 immutable 數據結構,比 facebook 的 immutable.js 性能好
  2. jcouyang/conjs · GitHub -- 我 fork 的 mori 版本,集成 core.async aka CSP aka goroutine

  3. cognitect-labs/transducers-js · GitHub -- transducer 單獨的 javascdript port,當然 mori 已經包含了
  4. 入(rù-lang) -- 我的experiment,結合 macro 與 mori

  5. facebook/react · GitHub -- virtualdom 隔離開 mutable 的 dom,革命性的讓前端編程也 immutable
  6. http://ramdajs.com -- underscore 你錯了 https://www.youtube.com/watch?v=m3svKOdZijA

  7. cujojs/most · GitHub -- Monadic Reactive 編程
  8. http://folktalejs.org/ generic functional programming, monad 的實現不錯


謝邀。。。

其實函數式作為一種編程思想並沒有多麼複雜。這種思想同面向對象啊,MVC啊,設計模式啊一樣可以帶給我們解決問題的好辦法,舉一個最簡單的栗子——

我們團隊維護著一個叫做NovaUI的移動組件庫——NovaUI-高性能的移動端組件庫

某一天呢,我在項目中需要使用其中的 Swipable組件 , 但是呢,我發現一個問題,按照產品設計,我的swipable元素有一個偽滾動條,這個滾動條要根據Swipable組件的滾動條狀態同步更新,也就是說我必須拿到組件當前的滾動狀態,而這個狀態在組件設計對外的介面中並沒有暴露出來。

那麼怎麼辦呢?無外乎有幾種辦法:

1) 放棄用這個組件來實現這個需求

2) 自己修改組件,將滾動狀態暴露出來

3) 讓團隊負責這個組件的同學升級組件

4) 利用函數式編程思想,不修改組件的情況下對這個組件的方法做一些額外的修飾

1)不說了,2)的問題是將來組件升級了我就不能享受新的功能,3)的問題是這個項目時間上不允許。因此我選擇了第四種方案——

var fn = {
watch: function(func, before, after){
return function(){
var args = [].slice.call(arguments);
before before.apply(this, args);
var ret = func.apply(this, arguments);
after after.apply(this, [ret].concat(args));
return ret;
}
}
}

var swipable = new Swipable({
element: ".swipable-wrap",
dir: "horizontal"
});

swipable._scroll = fn.watch(swipable._scroll, function(pos){
var p = pos / (swipable.min - swipable.max);
$("#progress-bar .current").css("width", Math.min(p, 1.0) * 240 + "px");
});

基本思路就是上面的代碼,首先定義一個watch的高階函數,它可以"watch"任意一個方法,在它被調用之前(before)和被調用之後(after)攔截進去。

然後我就watch了swipable組件中的_scroll方法,在它被調用前拿到了它的參數pos,進行計算,同步更新偽滾動條。

這樣的話我就在沒有對swipable組件本身改動一行代碼的情況下得到了我要的功能。

這種利用高階函數進行函數變換的思路,就是函數式編程的思路,這個例子就是用函數式編程思路解決問題的實際例子。它帶來的好處相信大家可以體會到的~

最後,給出完整栗子代碼在這裡—— swipable - w3ctech code

~~~~~~

更新:

我這裡再解釋一下上面的思路為啥說是functional的,我們知道,functional將函數本身看成輸入和輸出的映射,或者說是一種確定的運算。那麼我們可以定義兩個函數是否相等,對於任意函數a、b,如果給定的參數x、y,返回值z,當xa === xb且ya === yb時,總有za === zb,那麼我們認為a、b等價。當然,對於js的函數,除了這一點,還必須包括一個附加條件,那就是this上下文,也就是說,對於函數a、b當this上下文、參數都相同時,兩個函數總是返回相同的結果,那麼我們就說方法a和方法b完全等價。

可惜,以上這種解釋並不完全成立,這裡有一個「函數純度」的概念,對於常規的函數來說,每一次傳參確定,返回值是確定的,但可惜的是,這個規則並不完全成立,比如Math.random()、Date.now()這些函數每次調用的參數相同,返回值仍然不同,或者說,這些函數的「純度」比較低。那麼有這樣「低純度」的函數存在,我們就不能用上面的方法簡單來定義兩個函數「等價」

那麼如何定義任意兩個函數完全等價呢?我們可以有一個籠統的定義,也就是a、b等價意味著在系統中將a完全由b代替掉,系統運行的結果和原來相同。

看下面的範式——

function equal(func){
return function(){
var ret = func.apply(this, arguments);
return ret;
}
}

上面這個我把它叫做函數等價範式,這個範式是一個恆等範式。

也就是說,對於

someObj.someMethod = equal(someObj.someMethod);

不論someObj和someMethod如何實現的,上面的賦值語句都完全不會讓代碼運行結果有一絲一毫改變。

有了這個等價範式,我們確保不會影響函數運算產生的結果,而不用考慮函數運算的具體過程,而且,我們可以在這個產生結果的b過程中添加我們的干預,只要我們的干預並不去影響a的輸入參數和返回值,那麼這層「干預」並不會對系統造成任何不可預知的影響

function safeTransform(func){
return function(){
做一些事情,但不去改變 arguments
var ret = func.apply(this, arguments);
做一些事情,但不去改變 ret
return ret;
}
}

我們通過範式約定,用模式把對系統的影響控制在可控的範圍內,使得對系統的干預造成的風險可預知可控,而這一切,基於基礎的函數式編程原理。

大致上就這樣,實際上這個思路展開,還有很多好玩的可以討論的~


現在最成熟的是clojurescript。它是clojure的一個子集,可以編譯成javascript。clojurescript編譯出來的javascript可以使用google closure compiler進一步優化,advanced optimization可以做dead code removal,非常強大。正常人很難寫符合google closure規範的javascript,但clojurescript編譯出來的javascript很容易符合google closure規範。加上clojure強大的各種庫,好用到哭的數據結構和清晰簡單的語法,使撰寫前端成為一種享受(先適應,再享受)。項目越大,狀態越複雜,就越享受。

此外,react的思想被業界接受後,clojurescript上的reagent, om等react的封裝庫,由於clojure本身的immutability,使得om/reagent寫出來的react UI比reactjs自身用javascript寫的效率好了非常多。elm對此做了個比較:gen/pages/blog/blazing-fast-html。

另外一個函數式語言elm,是haskell的子集,也是compile to javascript,其本身就支持FRP(functional reactive programming),目前最新版本是0.15,還不算穩定版。elm的想法來源於其創始人Evan的博士論文,其中一個重要思想是signal,具體可以看:https://www.youtube.com/watch?v=JreO-Kl0Ed4。目前這個項目由Prezi支持。Evan幾乎每月都會搞一次Elm的hackathon,我還沒參加過。

兩個語言各有千秋,都有repl便於開發,也都可以邊開發邊調試(代碼改動立即反應在browser,clojurescript需要figwheel庫支持),學習曲線很長,一旦上手習慣,生產力會大大高於javascript。


(本經驗只適用於 JavaScript)

嘗試過一段時間在使用 JavaScript 中運用 FP,有一些想法。不知道是缺乏某些認知的原因還是 FP 的固有屬性,感覺要想完全在 JavaScript 使用 FP 其實是很困難的事情。能使用一些 FP 的基本思想,不要過分追求 FP 就挺好的。

初學的時候會嘗試強迫自己去寫符合 FP 思想的代碼,例如強迫自己強調函數的「純」,沒有副作用,強調函數組合(composition),強迫自己能柯里化的地方就柯里化。後來會發現這樣寫很累,寫出來的代碼也不盡人意。舉一個前陣子我 FP 中毒的時候的例子:

async createPost () {
const isEmpty = _.anyPass([_.isEmpty, _.isNil])
const isPropEmptyMaybe = (prop) =&> _.ifElse(
_.pipe(_.prop(prop), isEmpty),
_.always(Either.Left(`${prop} 的值不能為空`)),
Either.Right
)
const isValidatePost = _.pipe(
isPropEmptyMaybe("title"),
_.chain(isPropEmptyMaybe("content")),
_.chain(isPropEmptyMaybe("tag")),
)
const alertProp = (prop) =&> _.pipe(_.prop(prop), alert)
const sendOrAlert = _.pipe(
isValidatePost,
_.ifElse(
_.pipe(Either.isLeft),
alertProp("value"),
_.map(this.sendPost)
)
)
sendOrAlert(this.state.post)
}

要是這個項目是個多人項目,估計同事抓狂然後把我五馬分屍。其實它實現的功能很簡單:就是把內容發到後台資料庫之前先做驗證,普通寫法 if else 就完事了。但是我為了追求 pointfree,追求沒有非函數的變數,組合了一堆的函數,還用了 Either 的 Monad,同事要搞懂要明白什麼是 Monad 還有 _.chain 是什麼鬼。

我還模仿過 Lisp/Scheme 裡面的 cons/cdr/car 構建了一個用純函數寫的分頁組件,把數據都放在了參數上,完全做做到了函數就是數據。我為了做到 Pure,嘗試給 HTTP 請求和 DOM、BOM 的操作加上了了 IO Monad,然後發現代碼全是 Monad。

這樣的例子很多,都是過了一陣子連我自己都看不懂的代碼。所以後來我終止了這種腦殘行為,不再過分追求 FP。就像輪子哥所說的那樣:

  1. 我們的大腦已經 hardcode 了非 Funtional 的神經網路,適應起來很困難。
  2. 大部分人都是從非 Funtional 的 C 語言系出來的。很多 Functional 的概念都沒有掌握,新手學習成本很高,實際項目你需要培養新人需要很大精力。
  3. 前端其實都是跟副作用打交道,HTTP 是副作用、DOM 是副作用,你 console.log 也是副作用(真是讓人絕望啊
  4. 寫出來的代碼可讀性真的很差
  5. ...

所以後來我就總結了,把一些 FP 的思想結合我們原來的神經網路就挺好的,它是錦上添花的作用,而不是雪中送炭。例如:

  1. 寫一些可讀性好的純函數,測試起來很方便
  2. Currying 和 Composition 有時候還是很好用的
  3. Monad 學一些,但是不要用起來,因為別人看不懂(捂臉
  4. ...

這些經驗僅限於 JavaScript,其他的 ClojureScript、Elm 的經驗可以參考大大的答案。


我接觸到的類庫(不完整):

Undescore/Lodash 模仿 Haskell Prelude 庫的各種數組操作

React 模仿多了... 不知道..

Immutable-js 模仿 Clojure Haskell 當中的不可變數據

Bacon.js 模塊 FRP 的流, 雖然模仿得不像

Future/Promise 模仿 Haskell Monad 處理非同步的機制, Promise 在 Racket 里也有..

ClojureScript 模仿 Clojure

PureScript 模仿 Haskell

LispyScript 模糊 Lisp 語法

Redux 模仿 Elm

CoffeeScript 模仿 Lisp 系語言, 一切皆是表達式

ES6 尾遞歸優化, 模仿 Scheme

Sweet.js 模仿 Lisp 系的 Macro

Flow 語言, 模仿 Haskell 的類型系統... 雖然模仿別的更多

函數式語言幾個特點(不嚴謹):

* 函數可以作為參數使用, 高階函數

* 純函數概念, 可以沒有返回值以外的內容

* 不可變數據概念, 數據一經創建不可修改, 數據結構復用

* 模式匹配

* 代數類型系統

* Monad 對非同步, 對狀態的抽象

* 惰性求值

沾邊的應該還有很多...


函數式語言有個很重要的概念:immutable,翻譯過來大概是不變數的意思。Facebook有個很火的庫,叫做 Immutable.js;immutable 的好處都有啥?誰說對了就給他(facebook immutable.js 意義何在,使用場景? - JavaScript )

不知道曾經在哪兒看到有推薦這本書,貼在這裡:《Mostly Adequate Guide to Functional Programming 》 主要講 JS 的。


推薦我自己寫的電子書 《函數式編程樂趣(使用underscore.js)》,基本的map/reduce,集合操作,實例都有涉及。


UI = f(state)


ES6的arrow表達式借鑒了函數式編程里的lambda calculus。由於javascript語言層面一般是命令式、一半函數式,所以函數式的思想運用起來就特別多。比如: 函數是一等公民、閉包等等。另外coffeescript借鑒了lisp,livescript借鑒了haskell,如果有時間,用js擼個scheme解釋器或許你就清楚了。


明明是函數式編程語言,偏偏要模仿成面向對象語言來用!


rxjava


坐等張女神填坑


前端的邏輯就應該簡單,參考 UnderscoreJS。

源碼可讀性非常高。


難道不是js

(話說js的創建者就是搞lisp的、的


推薦閱讀:

Vue.js 如何添加全局函數或變數?
什麼樣的前端技術 leader 是稱職的?
為什麼優酷在PC端瀏覽器依然用Flash?
為什麼現在很多網站都不用原生的input輸入,而是用div模擬?
前端構建工具是什麼?

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