在遊戲開發領域,不考慮現有開發人員熟悉程度,函數式編程在設計上或者理論上有它的先天缺陷嗎?

討論可以包括Haskell(含Elm等),Erlang,Clojure等在內,如果要提及F#或者Scala(函數式特性部分)也歡迎。

以下兩點不在本問題範圍內:

----------------------------

* 我理解現在業內,尤其在中國,C++(具體而言是C++98 w/o template)是最大眾化語言,框架也多;

* 我理解現在多數框架都是基於命令式語言的;

我關心有什麼情況,函數式編程在抽象上,或者設計上有什麼硬傷,導致它無法很好的面向遊戲編程。

再次強調,不討論人員素質和現有傳統的限制,我們可以假設開發人員都是溫趙輪級別的人員,或者精確地說,是函數式領域對應溫兆輪的物種。

我用了Elm來寫了一個小遊戲,我覺得狀態之間的轉換非常的清晰,包括對隨機函數的處理(Elm 0.14的設計)和對外界輸入消息的處理,還有別的優點;

缺點目前我只看到兩個:

1、似乎對特定數據結構,如二維數組的維護,有一定的難度(也可能是我不熟悉某些pattern);

2、程序效率極大依賴底下虛擬機(編譯器?)的效率,比如你只是修改一個大數據結構的一部分,由於immutable的特性,這在語意上的重新構造一個大數據結構,底線是否這麼粗暴就完全依賴優化了。


我自己的看法(嚴重受限於我的認知)

遊戲開發

  • 首先,FP中利器,map、filter、reduce這一系列控制抽象手段,現有語言已經通過庫的方式提供了,比如C#的linq,在性能不敏感的一般場合,可以大量使用。雖然Unity當中使用linq還有點麻煩,不過我寫lua的時候,一直zip、map得很嗨呢。另外,純函數這個有悖OO的概念,由於可重入的優點,也是可以廣泛使用的。(樓上提到函數式能做的C++也能做得差不多,嗯,我覺得這方面C++有個關鍵性短板,GC!)
  • 在遊戲客戶端方面,遊戲邏輯中並沒有本質複雜的東西,既然問題都沒有,也就無所謂尋找解決方案;下層,遊戲引擎,出於性能需要,短期內似乎找不到C/C++的替代品。現在的手遊客戶端需要的腳本,輕量、熱更,lua等就滿足需要了;而具備反射能力又有靜態類型檢查的C#,能在編輯器方面提供支持,也是一個選擇;FP能夠通過immutable數據結構提供的並發能力,客戶端不需要...嗯,總的來說,做引擎,沒得選;做邏輯,什麼都可以。所以如果某個像Unity這樣風靡全球的引擎,推F#而不是C#,那你就可以看見FP普及了(不過如果Unity真選擇F#做腳本的話,以它的門檻,不知Unity還能不能有今天的市場份額...)。
  • 遊戲行業,尤其是手游、頁游,是一個比較奇葩的行業,因為產品周期短,複雜度不高,並不存在很多行業當中,由於10年+的老項目代碼需要維護,不能選用新技術的問題(當然,盈利5年+,賺得盆滿缽滿的少數產品除外,這種成功項目的技術選擇,穩定性、成熟優先)。之前很多年,遊戲伺服器用C++來寫,非同步模型、內存管理都比較痛,在C++中寫異常安全的代碼,坦白說,相對至少數萬的從業者基數,要求太高。所以啦,雖然遊戲伺服器規模不大(分服),而且相對的更屬CPU密集而非IO密集(對關係資料庫需求很低),但也的確像其他領域的伺服器一樣,需要比C++更容易駕馭、支持並發的基礎設施,所以,很多公司,還是在使用Erlang、Haskell、Scala了(雖然Go也被使用,但不屬於FP)。
  • 關於從業者的FP上手難度,其實我覺得不是問題。有實力的團隊,隊伍里總有好幾個極客,這種人,你把諸如FP這種好玩的東西扔給他,他眼裡是會放光的;他們連Unreal都敢吃,FP算什麼呢?至於說,大部分人,在學校里就接收的IP教育,寫起演算法總是for循環加mutable數組,讓他寫FP他會哭;沒事兒,我覺得程序員是最能接收新事物的人群,大不了頭一年他代碼寫得爛一點...其實我也一直以為上手門檻伴隨的人力資源會是一個問題,不過你看廣州、深圳那些用Erlang、Scala的公司不是活得好好的嘛...

函數式語言的優點

  • 優點自然是抽象粒度高,寫起代碼來要多爽有多爽;還有immutable數據結構帶來的並發能力,你看連Roslyn都以C#來寫immutable演算法了,說是比C++版本更快,恐怕FP的開銷其實沒那麼大?優點不說了,誰用誰知道,這裡問的是缺點,所以談缺點。

函數式語言的缺點

  • 缺點是性能。
  • 還是性能。
  • 更確切的說,是純函數演算法的缺陷,所以,我更喜歡F#、Scala的做法,我覺得,提供immutable數據結構,但同時也給你賦值的權利,這才是接地氣的,也能在性能敏感的地方,規避下面我認為的性能問題。當然,這些認識受限於我的知識,函數式專家和虛擬機專家,應該能對我遇到的這些問題作出解釋。

  • 惰性求值。雖然從理論上來說,惰性求值可以避開一些不必要計算,但這個優點恐怕只適用於少數特殊演算法,而如果大規模使用惰性求值,尤其是Haskell那樣的全文惰性求值,就我的觀測來說,thunk的狀態管理、額外的閉包捕獲環境的開銷,總的開銷是更大的。我不知道Haskell的虛擬機能以什麼手段來優化,但作為一個靜態語言,它在http://benchmarksgame.alioth.debian.org/中的表現讓我有點失望,我是覺得它應該更快(在我的某些測試中,它的性能甚至還比不上動態類型的Racket,而後者幾乎與v8一樣快)。所以,惰性求值,我認為只用在特殊演算法中比較好,像Scala、F#那樣提供一個lazy關鍵字或者call-by-name/need就夠了。
  • 迭代 vs 遞歸,基本不會有性能問題。
    • 在命令式語言中,大部分的演算法,適合用迭代來寫,也就是1、2個for循環,少部分演算法,比如對遞歸數據結構的訪問,可能用遞歸來寫更簡單。
    • 對於命令式中的迭代演算法,其每次迭代,都以上輪的輸出作為本輪的輸入,所以,它的遞歸版本,就是反覆更新狀態並作為參數來進行尾遞歸調用(類似於特化的fold);編譯器要將尾遞歸優化成循環,既可以像Scala那樣,進行語法樹層面的變換,也可以生成虛擬機支持的專門的tailcall指令(.Net CLR支持)。在前端做變換很容易,因為本質上只是修改參數並重新調用函數自身,所以,將它變成while循環,只需要將整個函數體嵌入一個大的while(true)當中,將原本遞歸調用的地方,改成「修改形參+labeled continue」即可。但前端變換的方法,比專門的tailcall指令限制更大,因為它只支持函數自身的尾遞歸,如果涉及多個函數交錯調用構成的遞歸,它就無能為力了。所以說,由於JVM指令集的限制,至少在目前,Scala的遞歸演算法(f1、f2交錯調用構成的遞歸)有可能不如F#表現得好。至於tailcall指令,記得看過一個提問,問為何C#沒有在合適的位置生成tailcall指令,編譯器開發組的人說,JIT過後的tailcall實現開銷不小,不鼓勵在C#中用遞歸代替迭代(其實我不太理解這個回復啦,開銷大總比不支持好吧)。
    • 命令式語言中的出現的遞歸演算法(沒被改寫成迭代,往往也就意味著它是非尾遞歸),直接在命令式語言中運行,由於棧消耗,它可能造成stack overflow,此時的解決方案是,尋找更複雜的迭代演算法來控制棧深度,或者手工維護一個狀態棧(空間消耗從棧上轉移到堆中);反倒是在純函數式語言中,由於不支持迭代,虛擬機必須保證這種演算法能運行(經典的quick sort就是非尾遞歸演算法),所以可能需要進行CPS變換,來自動的將棧的空間消耗轉移到堆中;所以,天然的遞歸演算法(難以改寫成迭代的時候),在函數式語言中反倒更好用,因為編譯器和虛擬機會自動將它處理好。
    • 總結下,因為一般意義的迭代,它的FP版本也都是尾遞歸,編譯器和虛擬機能夠優化的很好,所以開銷不大。
  • immutable數據結構的操作。虛擬機不是魔術師,它真的能夠把immutable演算法優化成更等價的mutable演算法一樣快嗎?嗯,看個典型的list_pushBack(Scala):

    def list_pushBack(l : List[Int], value : Int) : List[Int] = {
    l match {
    case Nil =&> List(value)
    case head :: tail =&> head :: list_pushBack(tail, value)
    }
    }

    從代碼來看,對一個長度為N的節點進行pushBack,會重新創建N個中間節點,他們的head保持不變,tail指向新創建的後繼節點。撇開代碼,想像一下,last節點的tail,為了指向新節點,而又不允許賦值,自然得重新分配;而倒數第2個節點,也必須重新分配使得新的tail指向新last節點...所以,如果我通過連續的pushBack構建一個100w個元素的list,會創建「1 + 2 + 3 + ... 100w」個中間節點,即O(n^2)的空間複雜度。什麼時候我會做這種蠢事?經常,比如遞歸當中的「list1 ++ list2」。為了避免這裡的平方開銷,可以使用Difference list等技巧(在Haskell、Scala中,我能得到預期的優化,但在Racket中無效,後者的虛擬機應該對此有專門優化);當然,也可以寄希望於虛擬機施展神奇的魔術來將它大幅優化(對於命令式語言的虛擬機如JVM,似乎連續調用如filter(map(zip))的數百萬次分配可以寄希望於逃逸分析和強力的YoungGC?還可能有什麼手段呢?函數式虛擬機怎麼做優化的?只能求助專家 @RednaxelaFX )。但至少從現實來看,Haskell虛擬機沒能讓這裡的開銷對我透明,Scala使用的是Java的虛擬機,也需要施展專門技巧。在我的一些函數式經驗當中,某些演算法本來開銷極大,一旦改用mutable結構(數組和字典)後,性能得到了數量級的提升,完全杜絕了不必要的分配。總結下,immutable數據結構恐怕並不適用所有的場合,如果不是為了控制並發編程當中的複雜度,語言開放mutable結構給我,非常有必要。

  • 純函數語言如Haskell,當演算法涉及緩存狀態時...State monad?反正我是很難受啦...本來簡單的問題,似乎越搞越複雜。
  • 至於說函數式相關的理論門檻?不知道算優點還是缺點...喜的是,學習的過程能讓我開闊思路、長見識;憂的是,我路子野,沒文化,吃力...
  • 雖然這裡是在列舉函數式的缺陷,但其實針對的都是純函數式做法,而像Scala、F#那樣,同時允許命令式編程和函數式編程多種範式,允許我在瓶頸的地方使用for循環+mutable容器,那麼,這種表達能力,純粹就是傳統命令式語言的增強了,幾乎沒有缺點。


優點,你可以搜索這篇虛幻3的主要開發者所寫文章:《The Next Mainstream Programming Language:A Game Developer』s Perspective》,分析涵蓋了遊戲編程的主要部分。

單看遊戲邏輯,函數式編程不太適合。遊戲中大量對象都存在著豐富的狀態,狀態間的轉換也非常頻繁且複雜,而對象之間的交互同樣如此,這不是函數語言擅長的領域。文中也認為這將是最困難的一部分,反而渲染,物理方面因為可看作數據並行或計算並行問題,同時這些工作很大部分已經交給了GPU,正是天生適合函數語言的地方。文中認為函數語言及編程範式對thread
parallel的良 好支持可以彌補這方面的不足,多線程帶來的總體效益提升最終超過單線程命令式語言(處理遊戲邏輯時)。老實說邏輯處理應該不會佔用太多時間片,除非是AI密集型的遊戲,而邏輯流程,如何分解到多個線程,這是難於處理分析的。

不過,無論是邏輯或渲染,許多計算的中間結果在遊戲編程中都是很有用的,傳統編程中可以緩存起來供後續使用,減少重複計算。這種手段對函數語言來說可能會很困難,常見的表操作並不適合保存中間結果(個人觀點,所知有限)。當然都使用函數語言了,對可能的內存多餘拷貝和計算重複的心理底限會低些。

我在《Game Engine Architecture》中看過Property-Centric Architecture這種Object Foundation的模式,我覺得按這種模式構造的引擎應該蠻適合函數式語言的。與組件模式有類似思想,但組件的粒度被分解的更徹底(更小)。函數語言通常是面向「計算」的,而越小的粒度越適合「計算」。非要給出理由我覺得越小的粒度其正交性越好,同時越是「可集合」的,從而利於「計算」。Property-Centric難以處理的是各個property的交互,message communication。我沒學過erlang,似乎erlang 的message傳遞可以作為一個例子?但通常一個property應該不足以達到被看做一個「進程」的程度。從我有限的接觸看,函數式語言對message,event機制並不友好。雖然說message分發仍然可以作為一個message list的迭代,event其實就是回調,對於一門函數作為值飛來飛去的語言,模擬回調簡直綽綽有餘,但message或event在傳統編程中往往是非同步發起的,以函數語言考量這很可能會要求打破當前表操作順序或改變表元素內容。不管怎麼說我覺得Property-Centric這個模式挺酷的。


目前,其實還是缺少庫,以及一些語言編譯器級別的特性支持。

1. 封裝良好的,mutable的數據結構,包括向量和矩陣是很有必要的。目前主要是沒有一個統一的矩陣庫(包括稀疏矩陣與滿矩陣)。社區里目前主要商業化方向在金融數值計算上,所以這塊的高質量庫,遲早會有的,應該不會太久。

2. 默認的Strict語義。7.10或7.12版的GHC,將有一個Extension,可以給予開發者控制編譯結果是否採用默認的strict語義,而非lazy。這個控制可以精確到每個文件。當然,我懷疑短時間內,能否把這個feature做到成熟。

樓上 @WBScan 提到的那個各種語言的benchmark測試,那個測試理論上是不能包含FFI與第三方庫的(實際上許多動態語言是做不到這點的,比如讓perl不用內聯c上正則看看?)。在真正性能敏感的地方:

1. 應該使用mutable的數據結構。Haskell里,可以象c一樣的去操作內存。

2. 內聯外部語言的庫是必要的。目前可以很方便的內聯C。


函數式語言做實時渲染還是壓力山大,做做光線跟蹤的大作業還好。

至於像Elm那樣的Functional Reactive Programming,應用還不多,遊戲也還是mario級別的,不過個人看好其在GUI方面的前途(包括遊戲)。

數組的問題的話,函數式編程本身就不應該維護可變數組,題主可以去試一下clojure,clojure內建的list/vector/set/map是函數式數據結構很好的例子。

最大的問題其實不是性能,而是社區一缺人二缺killer app,對比下go和rust的發展就知道語言優雅算個毛,有錢,任性。


唉,不說高大上的理論、執行效率和開發人員學習成本什麼的,我用Erlang做遊戲服務端開發,最大的感受就是:

策劃案的不確定性和函數式語言的數學式抽象要求是矛盾的

遊戲開發過程大家最苦惱的就是程序反覆跟著策劃案調整,作為有經驗的開發人員,一定不會抵觸這種行業現狀,也不可能要求策劃案想好後就不做任何調整,而是應當盡量幫助策劃早點明確思路,同時盡量想辦法降低調整的成本,並減少調整後出現BUG的可能性。

但是函數式語言的本質是要把邏輯和行為極度抽象,直到用一個個簡單公式一個個嚴謹的模式就可以表達業務邏輯。

當一個業務需求還沒定型的時候就要做高度的抽象是非常有難度的,而且過早的抽象也導致調整起來很麻煩。

以上是個人愚見,還望指正。


並沒理論上的缺陷,但是有實際上的缺陷。喜歡Haskell的同學,總是喜歡說理論,但是忽視了實際。


你要明白, 一個新技術, 相對於已有的成熟技術, 如果沒有壓倒性/顛覆性的改革, 那麼是沒有必要切換的.

比如互聯網領域, MySQL就不會被其它關係型資料庫取代.只會在某些場景被其它模型的資料庫取代, 比如列存, 因為這是質的改變.

而對於遊戲開發, 甚至其它領域的開發, 很多時候, 人們要的只是 比如把函數像值一樣傳遞, 那用Lua好了, 甚至C++本身也可以玩高階函數.currying, 等等技巧.

另外, 對於純函數, 任何語言都可以寫, 只是一些函數式語言可以由編譯器檢查而已.

對於immutable, const不就可以起到差不多的效果.

其他不一一列舉.

所以說, 函數式語言能做到的而且大多數場景需要的東西, C++, java也能差不離, 但過程式語言能輕鬆做到的, 你用Haskell有時就非常蹩腳,

所以就算要換, 也應該換C#或者Scala, 而不是Haskell.另外, Erlang寫服務端可以, 用來寫遊戲邏輯, 很多人會覺得還不如Lua.夠簡單, profiling方便.

總之, 你面對一個場景, 先考慮流暢的表達邏輯需要哪些語言要素, 然後明顯很多人覺得遊戲開發需要的要素, Haskell/Clojure缺的多了點.當然, 如果寫寫parser, 很多人會覺得Parsec就比C++的一些工具強.


做遊戲開發的不要指望有fp用,網頁遊戲除外。


Haskell對使用者的數學要求已經嚇退了一堆人了,又不是每個人都是高斯,伽羅華


修改大數據結構,不會導致數據大量複製,參見 Bagwell 的 HAMT。

clojure Scala 都是使用的這個數據結構


最大的問題是性能缺陷。。現在的機器都是諾依曼體系,硬體流水線體系,本來就適合流水線命令式樣的軟體語言。。除非底層硬體架構大改變,否則函數式不能在要求性能高的領域成為主流。


推薦閱讀:

單片機多位元組除法怎麼實現?
為什麼大家都能接受2D橫版遊戲鏡像翻轉後人物左手持武器這種設定?
能不能設計針對確定數對的通用轉換函數?
Passphrase,Passcode,Password 三者之間有什麼區別和聯繫?
長得漂亮的女程序員,如何在逛街時,讓別人覺得自己的職業是程序員呢?

TAG:編程語言 | 遊戲開發 | 編程 | 函數式編程 | Haskell |