函數式語言開發 GUI 是不是自虐?
我用java搞GUI的時候依賴很多副作用 比如
按一個鍵 主要進行通信 然後 在附帶的刷新一下界面
這樣的話是不是用函數式語言描述起來很彆扭?
不是。搜Functional Reactive Programming有真相。
另外,FRP的概念被亂用太多,比如很多人誤以為Rx就是一個FRP系統,而其實根本不是。Rx是基於離散的Event Stream的,而FRP里的一等公民不是Event,是連續的Behavior。基於連續的Behavior;有嚴格的指稱語義;滿足這兩點要求的引擎才是FRP。
感謝 @neo lin提醒,還是加一些文獻引用吧。FRP的始祖論文是Conal Elliott和Paul Hudak的Functional Reactive Animation,不過文章年代久遠,不太好讀。建議從http://dl1.frz.ir/FREE/papers-we-love/functional_reactive_programming/a-survey-of-functional-reactive-programming.pdf這篇文章讀起,以及參考Conal Elliott今年的講座The Essence of FRP。開發方面,可以參考gelisam/frp-zoo · GitHub。其實,我對於提到 FP 實現 UI 就馬上聯繫到 FRP 有點看法。所以寫了一篇:
Functional UI Programming
先針對「搞GUI的時候依賴很多副作用」簡短回答:model-view-controller 里的某種 style 推薦 stateless view,所以至少 view 的部分 render 是應該可以去掉副作用的。
最近擠出些時間來看 Haskell 和 Functional Reactive Programming。由於主要工作領域和興趣都是 user interface,所以就一知半解地寫寫用 FP 實現 UI 的想法。關於這方面入門者的困惑很多,我覺得有兩個主要原因,第一是 FP 社區本身對 UI 領域投資不多;第二是比較熟悉 FP 的人談及 UI 的時候必然會提到,而且往往只提到 FRP。這第二點讓人產生傳統的 model-view-controller 不適合 FP 的錯覺。
Model-View-Controller Recap
談論 FRP 前先回顧一下在我本人經歷中佔主導地位的傳統 MVC 模式 [1]。這種模式中的 V 是 stateless view。從 model 發送到 view 的唯一通知是「model 發生了(詳情不知的)改變」。Stateless view 可以天然的被看作 FP 意義上的函數,參數是 model,輸出是整個或部分 UI 的 bitmap(或者說是 render 系統的 render command 序列)。
這個模式下的 controller 和 model 也都幾乎可以看作函數。如果採用 FP 模式,model 不能是保存狀態的「對象」[2],而是變成一個 immutable 文檔的列表。這樣做並沒有初看上去那麼複雜:現代基於文檔的 app 都要支持 undo/redo,所以如今的 model 已然無論如何要花力氣實現文檔列表(這個序列里的新文檔通常是對舊文檔進行 copy on write 得到,如果 FP compiler/runtime 實現得當應該可以達到同樣效果)。
如上所述,我的最初感覺是傳統 MVC 能夠並不費力的和 FP 模式吻合,所以一再聽到熟悉 FP 的人說 FRP 是 FP 在 UI 領域的主流甚至唯一解決方案讓我有點驚訝,懷疑是否之前的想法過於簡單化。
控制項化 UI
上文談到「傳統 MVC」時所說的 view 其實和大多數人腦中的稍微有些不一樣。很多 UI 是由現成的控制項 (toolkit control) 組合而成。而傳統 MVC 的 view 是指由 render 系統生成的一塊 bitmap。舉例來說,直接基於 Cocoa 和 MFC 的 NSView 和 CView 實現的 custom view 更符合 stateless view 的特性。如果你的 app 里沒有 custom view,而是完全由 built-in control 組合而成,那麼最外層的 container view 接受 model 之後的輸出就不是 render command 序列,而是把整體的 model 分成不同部分來更新 inner view 的 model。在《MVC:用來打破的原則》的最後一節談到了這種嵌套 MVC 結構。
控制項化 UI 讓很多程序員產生了一種「錯覺」,就是 MVC 里的 view 的行為不是 render bitmap,而是把 model 的某部分同步到某個 property 上去。而 view 也變成了需要維護自身狀態的對象。其實這只是看問題的角度不同。當你要自己實現足夠複雜的 custom view,例如一個 editable canvas 時,仍然要回到基本的 stateless view 模式。經常寫 custom view 的人處理大量 controls 的時候也把 controls 的更新看作 container view 的一種 render 行為而非數據的同步。
Functional Reactive
這時回來看 Functional Reactive Programming,它是更符合控制項化 UI 的一種解決方案。FRP 所構建的 DAG 的末端和 control 相聯的 event 或者 behavior 是和這個 control 自身的 model/proprty 的粒度直接對應。當整個 app 沒有一個統一的 model 而僅僅用 controls 自身的 model 集合來維護所有狀態的情況下,FRP 的 DAG 解決了這個 model 集合的同步問題,從而構建了一個 virtual global model。在傳統 MVC 里經常提到 model 要負責 data integrity,DAG 正是實現這個任務的一個特定的形式化方法。
基於這個分析,可以總結關於 FRP 的三個結論。第一,FRP 解決了離散的 model 集合的同步問題,這只是 MVC 中 M 的部分。把 FRP 看作一個 UI 解決方案是忽略了 built-in control 所做的 render 工作。FRP 是一個 model 同步方案。
第二,可以通過在 app 中保持一個集中化的 model 來避免使用 FRP 的 DAG。集中化的 model 更符合傳統的 MVC,從而也可以通過 stateless view 來採用 FP 模式。但這不代表構建集中化的 model 就一定是更好的做法。因為在控制項化 UI 里強行採用 stateless view 模式意味著蠻力複製很多沒有變化的數據。如果 FRP 的理論足夠紮實,它的 DAG 似乎是更優雅的方法。而且即便真的採用了集中化的 model,仍然可以在內部採用類似 DAG 的方式來保證 data integrity。
第三,當 UI 中的某個 view 足夠複雜而無法由 built-in control 來實現的時候,至少在這個部分必須回到傳統的 MVC 模式。所以 FRP 和傳統 MVC 實際是在 UI 實現里互相補充的兩個部分。傾向於哪一個的決定往往更多地取決於系統性能等等非架構因素,而並非由系統的架構硬性決定,也不是和 programming paradigm 綁定的。
腳註:
- 關於這方面我寫過幾篇 blog(一,二,三)。
- 在《MVC:用來打破的原則》里說過,model 其實有「反對象」的特性。
我補充一個: https://github.com/omcljs/om
Immutable virtual dom 開發帶撤銷功能的複雜 GUI 簡直爽
GoyaPixel 這種工具用 ClojureScript 實現非常的簡單, 但如果用 Java 搞, 得多十萬行還丑還慢
開發GUI,當然還是面向對象用的爽。
使用GUI嘛,還是reactive programming爽。
所以奧義就在於,你用C#寫GUI,然後封裝出一個為F#優化過的API,這樣大家都爽了。
如果用 Binding.scala 就不是自虐,而如果用其他 MVC 框架或者 FRP 框架嘛,雖然談不上自虐,但都會因各種坑爹問題而被虐。
這些問題包括:如何更新視圖,如何轉換狀態,如何註冊和釋放事件。
GUI 的難點在哪裡?
用靜態 HTML 描述靜止的界面不難,很多不會寫程序的 UI 設計師 HTML 和 CSS 都用得很熟。
難點在交互。
一段描述交互的程序,除了描述某一時刻的界面之外,還需要描述另一個維度:時間。時間線並不是簡單的直線,而是枝繁葉茂,層層分叉。
比如說在界面的某一時刻,用戶可以點確定按鈕,也可以點取消按鈕。用戶點擊確定按鈕或取消按鈕後,又會出現不同的界面。那麼這裡用戶選擇點擊確定按鈕,還是取消按鈕,就相當於時間線上的分叉。
怎麼才能在一段程序代碼中既描述某一時刻顯示的模樣,還能描述未來的各種變化呢?
傳統 MVC 如何描述交互?
傳統 MVC 給出的答案是把「顯示」和「時序」二者分離,前者就是 View ,後者是 Controller ,然後額外引入 Model 層,表示要顯示的狀態。那麼在 MVC 架構中,View 通過監聽 Model 變更來「渲染」出界面,當用戶在界面上操作時,View 通知 Controller , Controller 則根據時間變化,修改 Model 。這樣一來,每次事件,都會在 MVC 三者上轉個圈,每轉一圈,整個界面就切換到下一個狀態,顯示效果也隨之改變。
可以看到,我們編寫 MVC 每一行代碼的目的都是描述數據流動的「過程」,諸如事件如何傳遞、View 如何根據事件進行重繪之類。
遺憾的是,正常人類很難理解「過程」,而只能理解「樹」和「圖」。- UI 設計師可以用 HTML 的 DOM 「樹」,外加 CSS 來描述某一確定時刻的界面原型,但幾乎沒有 UI 設計師有能力寫一個 View 的代碼,描述界面如何根據 Model 的改變而更新的「過程」。
- 產品經理可以用用例「圖」描述時間線上的狀態改變,但幾乎沒有產品經理有能力編寫 Controller 的代碼,描述如何根據用戶事件來更新 Model 的「過程」。
幸好,我們專業軟體工程師,勉強能一邊自虐一邊理解「過程」(雖然經常寫出內存泄漏或是狀態錯亂的代碼)。我們前端工程師把界面原型和用例圖翻譯成「過程」,就是過去二十年我們能領工資的依憑。
數據綁定
在我們自虐的過程中,我們發現有一門技術,可以緩解我們自虐時的痛楚,這門技術就是數據綁定。
AngularJS、Adobe Flex、ReactJS 、 Binding.scala 都提供了數據綁定功能,比如我們可以用 Binding.scala 這樣寫:val source = Var(1)
@dom val destination = source.each * 2
assert(destination.get == 2)
source := 10
assert(destination.get == 20)
那麼我們發現,有了數據綁定表達式,我們就可以不再手寫事件處理函數,而只要聲明一些「數據綁定表達式」,可以讓源頭數據發生改變時,存放結果的地方就自動改變。比如上面的代碼中「@dom val destination = source.each * 2」 就表示讓 destination 的值永遠固定等於 source 值的兩倍,只要修改 source 的值,destination 的值就會隨之修改。
利用數據綁定,我們可以方便編寫 View ,讓 View 上渲染結果始終和 Model 一致。比如 Binding.scala 甚至可以在數據綁定表達式中進行循環:class TagPreview(tags: BindingSeq[String]) {
@dom
def render = {
&
-
{
- {tag}& }
for {
tag &<- tags } yield &
&
}
除了 Binding.scala 之外,其他幾個框架提供的數據綁定功能雖然勉強也能用,但或多或少都有些毛病:
- Flex 的數據綁定表達式太不靈活,不能像 Binding.scala 遍歷容器。
- AngularJS 的數據綁定性能太差,改個小地方就會刷新整個組件。
- ReactJS 的數據綁定主要靠虛擬DOM differential。ReactJS並不知道你修改數據的意圖,因而ReactJS框架本身不能精確的得知虛擬DOM和真實DOM之間的映射關係,而需要用戶手動指定 key 之類的屬性幫助 ReactJS 猜對。
姑且不論這些框架的缺陷,至少有了數據綁定之後,至少可以讓 View 寫起來跟靜態 HTML 差不多。實現 View 時,把 UI 設計師提供的靜態界面原型拿來改改就能交差了,而不需要再手寫更新 DOM 的代碼了。
那麼現在自虐的程度就減弱了一半,可以用人類容易理解的樹而不是過程來描述界面了。
時間線
現在還剩下另一個問題:如何描述一棵枝繁葉茂的時間線?
舉個例子,我們想要做個專家系統,用來預測約會後是否會啪啪啪,具體流程圖如下:
我們希望根據這張流程圖做一個調查問卷頁面,用戶每回答完一個問題,頁面中就產生新的問題,直到確定用戶是否啪啪啪為止。而且我們還希望,當用戶修改舊問題的答案時,頁面中的後續問題也會隨之改變。
比如給用戶展示問題「是第一次約會嗎?」,如果用戶選擇了「是」,頁面上就顯示下一個問題「你富可敵國」。如果用戶把「是第一次約會嗎?」修改為「否」,頁面就改而顯示「在哪兒的約會?」。
我先說結論,這種交互,用 Binding.scala 來做的話,非常簡單,自己看這個 DEMO 吧:
https://thoughtworksinc.github.io/Binding.scala/#2
頁面左邊是啪啪啪專家系統 ,右邊是源代碼。
你們先理解一下這段代碼的奇妙之處,我年後再來講解。(未完待續)
不是只有面向對象的語言才表達『對象』的概念;結構化語言可以(請忽略語法):
struct {
char[]* name,
char[]* text,
func* onclick
} Button;
function clickHandler () { ... }
Button myButton = ...
myButton-&>name = ...
myButton-&>onclick = clickHandler
函數語言也可以(類lisp語法)
(button name "myButton"
text "myText"
(onclick (lambda (prog
(process something)
(trigger something)))))
不只是語言表達能力的問題,你得設計一個支持非同步執行的運行時。OOP的思想有了property和behavior的區分,貼合GUI部件的內在屬性更加緊密:
Button myButton = new Button()
myButton.name = "..."
myButton.text = "..."
myButton.onclick = ...
足夠KISS了。
「趕deadline所以學一下旁門 angular.js,以後還是應該回歸正道用 elm」——宋教授(。。
Elm
hoodle,Haskell+Gtk寫的手繪軟體
http://ianwookim.org/hoodle/gallery.html
有FRP
最近在學Elm 有同好可以一起研究一下
任何能搞出結果的語言從結論上都可以用
但是另一方面,各種語言的效率和可維護性有好有壞。那就看你樂意不樂意了。
軟體工程學的SOLID原則在這種時候還是很重要的。比如責任分離之類,你用某些語言顯然就比另外一些語言容易達成。如果你故意去用不好用的語言,那的確就是自虐沒錯。題主可以試試 Cycle.js 就知道爽不爽了,整個系統就是一個函數閉環,通過 startWith 作為初始數據,通過訂閱 DOM 事件和網路請求事件來驅動 DOM 的更新。
here"s all the code u need to produce a todo gui app```
import Html exposing (div, button, text)
import Html.Events exposing (onClick)
import StartApp.Simple as StartAppmain =
StartApp.start { model = model, view = view, update = update }model = 0
view address model =
div []
[ button [ onClick address Decrement ] [ text "-" ]
, div [] [ text (toString model) ]
, button [ onClick address Increment ] [ text "+" ]
]type Action = Increment | Decrement
update action model =
case action of
Increment -&> model + 1
Decrement -&> model - 1
```
which inspires reduxhttp://xmonad.org/
用函數式語言Erlang實現的3D建模軟體Winds3D,知乎上有個關於它的問題,「winds3D為什麼會選擇erlang?」,http://www.zhihu.com/question/22138661,看完樓上給出的論文,最好再看看實際案例。
scheme一般是到開發gui時就先開發一套面向對象系統如mitscheme的oops和chez scheme的swl,前期叫做scheme++,一聽名字就是模仿c++。
轉開發語言容易,轉開發思想難。
就好像有些人就算是用著面向對象的語言,寫的仍然是面向過程的程序。
現在需要學習面向函數的程序,怎麼寫?只有一個,先寫再說。
我在腦補LOGO語言的小海龜拖著尾巴一步一步畫GUI界面的樣子。。。咳咳求摺疊
redux大法好
Reactive Cocoa
你把老vb放在哪裡了?至少不能說他是oo的吧
推薦閱讀:
※有哪些支持多種語言的 IDE ?
※各種編程語言的實現都採用了哪些垃圾回收演算法?這些演算法都有哪些優點和缺點?
※全python項目,使用protobufThrift適合嗎?
※Haskell中的惰性求值如何實現?
※Facebook 新發布的 Hack 語言怎麼樣?