使用函數式編程語言 ELM 開發游戲
使用函數式編程語言 ELM 開發游戲
我將會寫一系列關于使用elm做游戲開發的文章,這是第一篇。這是一種即時編譯成html和javascript的語言,因此你可以直接部署到web或在nw.js上將其打包,創建一個獨立的app或者游戲。它沒有太多的教程,但是很多東西我們必須學習我們才能進步。自從elm在開發圈子里活躍起來以后,為了防止文章過時,我將會一直更新文章,并且當存在任何錯誤時,我會改正。
函數式編程(FP)是一個讓人摸不著頭腦的典型范例。它的很多東西對很多人都是那樣,但是它卻代表了一群渴望寫出更簡單代碼的人,他們避免強編織(complecting)并創建易于調試的軟件。
Games
從很多編程的領域看來,游戲開發是最適合面向對象的而且游戲總是帶很多狀態。從表面上看,面向對象這種方法很適合。當我使用 Functional Programming的時候,我發現這種方法也很適合游戲編程,而且我也對如何解決問題很感興趣。
為什么我選擇用Functional Programming開發游戲呢? 簡單的說,我對傳統的軟件設計方法感到厭倦。命令式的面向對象代碼對導致一下過度設計的問題,而且很不美觀。
這僅僅是我的個人觀點,所以請放松,但是如果你渴望一些不同的事物,為什么不來functional的路上看看呢。
Iteration
看看下面最簡單的js例子,對于一個數組取平方。使用"命令式"的代碼你會描述一些將要發生的事情。而用functional代碼(聲明式編程的子集),你去描述你想要做的事情。所以"命令式"的風格中我們會定義一個臨時的index變量然后創建一個循環,遍歷一遍數組,然后每個value取平方。
var numbers = [1,2,3,4,5,6,7,8,9], i; for (i = 0; i < numbers.length; i++) { numbers[i] *= numbers[i]; }
相比起來,functional的方法路線呢,如果你寫js可以考慮下lodash這個庫。然而使用一個專門是functional programming的語言,會比的很容易,所以我們使用elm來做這件事。
import List (..) square : Int -> Int square n = n * n numbers : List Int numbers = map square [1..9]
使用elm我們不需要定義臨時變量,我們也會創建一個數組更容易,而且定義一個function復用。通過map我們將每個list的數字取平方,然后返回一個新的list。
正如你看到的,functions是對于傳進來的每個變量有類型提示的。import List (..)這一行簡單引入了核心list function,elm自帶的這些function 提供了 map和filter的方法。
譯者注: loadash的functional 路線
var _ = require('lodash'); var square = _.curryRight(_.map, 2)(function(n){ return n *n; }); square([1,2,3,4,5,6,7,8,9]);
Filtering
現在設想一下我們想從數組中移除奇數,然后只平方哪些過濾后的數組。通常,“命令式”的js是這樣寫的:
var numbers = [1,2,3,4,5,6,7,8,9], squaredNumbers = [], i; for (i = 0; i < numbers.length; i++) { if (numbers[i] % 2 == 0) { squaredNumbers.push(numbers[i] * numbers[i]); } } numbers = squaredNumbers;
當然,這樣寫可以用,但是定義另外一個數組看起來很亂,而且我們還是得寫循環。這就是"命令式"代碼最不具有新意的地方。你可能會在你的代碼的1000個地方重復寫上面的代碼。
現在我們試試functional 的寫法:
isEven : Int -> Bool isEven n = n % 2 == 0 numbers : List Int numbers = map square (filter isEven [1..9])
我們在numbers里增加一個filter,然后這塊方法就變得可以復用了,我們不需要對于有點不同的需求寫重復代碼。
這里就是functional programming閃耀的地方了,你花了更少的時間在編寫想要的做的東西上,而且代碼閱讀起來也很方便。還有就是這證明了方法是可以鏈式調用的。
Chaining
如果你現在覺得方法的nest調用會變得失控,你是對的。在elm語言中我們可以使用 |>操作符去幫助我們鏈式調用方法。
|> 操作符是 functional 程序的別名,它取得左邊所有的參數然后傳遞this當作最后一個參數,this是最右邊的參數。仍有<|是反向作用上述過程的。
-- this 1 |> add 2 -- is equivalent to this add 2 (1)
當有多個function被調用,我們很容易看到這個的好處。
-- this 1 |> add 2 |> add 3 |> add 4 -- is equivalent to this add 4 (add 3 (add 2 (1)))
這樣做減少了需要寫的括號,而且使得代碼更易讀,變得更像一句話:
numbers : List Int numbers = [1..9] |> filter isEven |> map square
Composition
一個更好的解釋composition的地方,將簡單的functoin組合起來編寫成復雜的。
在elm理我們可以將function compose起來,通過>>操作符。這樣做的好處是我們不需要指定input就可以提前把function compose起來。
-- this (isEven >> not) -- is equivalent to (\n -> not (isEven n))
從邏輯上來考慮,如果我們知道 g : A -> B 和 f : B -> C(譯者: 方法g是從狀態A -> B, 方法f是狀態B -> C),我們可以將f和g compose起來通過創建一個g >> f ,就是 A ->B ->C,(而且這個順序可以是反向的 f << g : A -> C)。
在這個例子中中我們檢測數字是否是奇數:
squareIsOdd = square >> isEven >> not -- `not` 是一個built-in的方法用作布爾值反轉 squareIsOdd 3 == True squareIsOdd 7 == False
這個inputs給了squareIsOdd一個 composed的方法,每一個方法調用時都返回了一個結果,用作下一個方法的參數。
State
State是程序儲存的狀態,通過對象中的變量來表示。問題在于state是這樣存儲的,它能夠允許開發者不在當前的scope下修改變量的值,這么做是存在隱患的,比如:
var foo, bar; foo = { "baz": 1 "setBaz": function(value) { this.baz = value; bar.qux = value * 2; // 討厭! } }; bar = { "qux": 2 }; foo.setBaz(2);
可能有一些修改bar.qux的原因,比如bar.qux應當永遠是foo.baz的兩倍。但是直到開發者看了setBaz的源碼之后他們才會知道bar.qux改變了。對象的api騙人了,這個例子是一個你可以明確的,容易的,識別出的糟糕代碼。但是這樣寫"相當有效",所以不可避免的導致程序員這寫。我自己看到過和這么寫過太多太多這種代碼。
所以,如何解決問題呢?不為開發者提供這些功能就行。elm沒有全局變量,沒有變量,只有input和output。
然而如果function沒有任何update操作,僅僅只是返回了input,output和input是同一內容,這樣避免了不必要的拷貝。
noop input = input sameAsInput = noop { a = "b" }
所以取setBaz這個例子:
type alias Foo = { baz : Int } type alias Bar = { qux : Int } foo : Foo foo = { baz = 1 } bar : Bar bar = { qux = 2 } setFooBaz : Int -> Foo -> Foo setFooBaz baz' foo = { foo | baz <- baz' } foo1 = foo |> setFooBaz 2
我們看到setFooBaz是沒有辦法修改bar.quz的。這個方法是沒有辦法修改scope外面的值的,所以只能返回一個新的foo.
澄清一下,你可能這么想foo: Foo是一個type為 Foo的變量,但是它不是。它只是一個function不需要input然后output一個對象。我們可以很容易改變一個東西成Foo: Int -> Foo,去讓baz被實例化成一種value。
如果我們仍想要確保bar.qux被及時更新成為foo.baz的兩倍,我們可以創建一個方法,這個方法是被兩個方法compose成的,被增加了2次人后返回原來的對象。
type alias FooBar = { foo : Foo , bar : Bar } fooBar : FooBar fooBar = { foo = foo -- our previously created `foo` function , bar = bar } update : Int -> FooBar -> FooBar update baz fooBar = { fooBar | foo <- fooBar.foo |> setFooBaz baz , bar <- fooBar.bar |> setBarQux baz * 2 } fooBar = fooBar |> update 2
我們能夠update這個值,向我們想的那樣,但是沒有副作用。update操作的output值包含了各種操作的影響。
Elm
在我看來,functional語言有很多優勢,通過上面的例子。閱讀、debug、復用都很方便無副作用。所以為什么不去試著在寫一個游戲呢。