想在JavaScript 裡面用Pattern matching?

blank

想在JavaScript 裡面用Pattern matching?

作為一名合格的前端程序員,我們當然不能停止學習(滑稽),畢竟tc39 還在不斷給ES 加feature(聽說private 要正式進標準了)。在一眾花里胡哨的語法裡面,像pattern matching 這種語法,還是挺讓人期待的,特別是對於稍微接觸過函數式語言的同學。

TL;DR:現在在JS的pattern matching還不能用。

要解釋pattern matching 的話我不是很在行,而pattern matching 則被一眾帶函數式語言(ML 系/Haskell/Elixir/Racket)廣泛採用。 Pattern matching 的優勢就在於可以讓繁瑣的if else 語句變得清晰和有條理。這裡我用前端比較火的ReasonML 來舉例,下面ReasonML 代碼(來自Reason 官網):

letoptionBoolToBool=opt=>if(opt==None){false;}elseif(opt==Some(true)){true;}else{false;};

改造成等價的pattern matching 寫法:

letoptionBoolToBool=opt=>switch(opt){|None=>false|Some(a)=>a?true:false};

我們發現代碼量少了很多。上面的代碼種,pattern matching 會一行一行地進行match pattern,第一行先對比opt 是否等於None,不等於就match 第二行,看看是不是Some(某個值),pattern matching 的強大之處就是match 之後還可以把Some 裡麵包住的值提成變量a,相當於解構,讓我們可以在接下來的代碼裡面使用它。

而在如ML/Haskell的函數式語言裡,pattern matching可以和ADT(Abstract Data Type)相輔相成,有很強的表達能力,而編譯器也可以針對pattern matching做相應的編譯優化,可以達到不錯的性能。所以說,pattern matching這個語法糖可以說是很甜很好用啊, 連C#也忍不住加上了

回歸到JavaScript ,雖然tc39很久之前就有關於pattern matching的提案,但是進化的路程是比較坎坷的,各種語法層面上的紛爭從來沒有停過。比如關鍵字(match...with 還是case...when),和符號(->/~>),以及是否支持else clause(可以match 所有情況的clause)等等等等。當然最終最變成什麼樣子還是由tc39 說得算。

現在的提案是這個樣子的。其實從現有的提案來看,pattern matching某些方面和解構是很像,但是能力比它更強,也和switch有部分重合(既然這樣為什麼要用case關鍵字呢)。想像一下我們可以這樣寫一個redux 的reducer:

functiontodoApp(state=initialState,action){case(action){when{type:'set-visibility-filter',filter:visFilter}->return{...state,visFilter}when{type:'add-todo',text}->return{...state,todos:[...state.todos,{text}]}when{type:'toggle-todo',index}->{return{...state,todos:state.todos.map((todo,idx)=>idx===index?{...todo,done:!todo.done}:todo)}}when{}->{}// ignore unknown actions}}

上面的代碼每一個when會進行一次match,when後面的結構就是pattern,只有pattern match成功,才會執行箭頭後面的語句,和switch語句不一樣的是,後面的語句執行完,不會繼續match,所以也不需要break 。所以顯而易見地,上面代碼每個when 都檢查action 裡面的type 是否可以match,然後把action 裡面其他property 給解構出來。同樣地,我們可以多層嵌套pattern,像第二個clause 一樣。

同樣提案也支持if predicator和initializer,可以說是很強大了:

case(x){when{x}={x:1}->...when{x:x=1}->...when{x:1,y}if(y<=10)->...when{x:1}->...}

如果寫過函數式語言的同學可能會對第一個例子裡面的return 比較不理解,這裡的return 其實是return todoApp 這個函數的,同時這個case 是一個語句(Statement)而不是表達式(Expression),這跟所有函數式語言都不一樣。當然演化成這個形態的歷史故事就很長了,這個proposal一開始也是像reasonML那樣把pattern matching當成一個表達式使用,大概在一年前我還在babel的一個PR裡面實現了這個最原始版本的pattern matching

var test = { a : [ 1 , 2 , 3 ], b : { bb : "ok" }, c : [[ 1 , 2 ], [ 3 , 4 ]], } // nested assert . equal ( match ( test ) { { c : [[ 1 , 2 ], [ d , 4 ]]} : d , // match 3, and bind d to 3 else : "foo" }, 3 );

在這個時候,match 完全是一個表達式,也就是,你不能在裡面return/break/continue 外面的結構。後來tc39 負責這個proposal 的champion 換了,新的champion 把它改成了一個語句(同時也把關鍵字match 改成了case),也就是上文提到的那個模樣,理由是JavaScript 始終不是一門expression 為主的語言,分支控制依然是關鍵(如果做成表達式就沒辦法控製程序分支了)。現在你可以在一個pattern matching 內部break/continue/return 外部的結構,一個clause 只是一個block(由一個或多個語句組成) 不是一個函數。

而用現有的庫模擬早期基於表達式的pattern matching也是可行的,唯一的問題就是比較醜而且速度太慢: zkat/pattycake 。 Patty cake 是一個模擬pattern matching 的庫(就是由那個champion 寫成的),可以以一種比較醜陋的方式模擬pattern matching,可以在babel PR 真正完成之前體驗pattern matching。

雖然最新的pattern matching實現依然沒有merge進babel: Pattern matching by gnprice · Pull Request #9318 · babel/babel ,但是大家仍然可以手動構建分支,或者在REPL裡面體驗pattern matching,我相信大家很快就能用上。如果有興趣的,甚至可以繼續完成這個PR,review 我的代碼(誤)。

性能方面,JS版本的pattern matching會有不少overhead,是因為編譯出來的JS裡面會有不少關於undefined的判斷,但是我相信一般情況下這些判斷不會成為瓶頸,相比手寫if else它甚至會顯得更安全,因為比如match 一個object 裡面的property 已經意味著它不為undefined。

總的來說,我還是特別期待pattern matching 這個feature 的,特別是用於寫React/Redux 的時候,可以讓代碼特別乾淨簡潔。但是這個proposal 目前只處於stage-1 階段未來仍有改動的可能,而babel 的PR 尚未完成,尚未能在自己的項目裡面用上。

等吧。

What do you think?

Written by marketer

blank

第二屆SEE Conf 2019 精彩回顧(附PPT 及視頻)

blank

給2019前端的5個建議