使用函數式編程語言 ELM 開發游戲

jopen 9年前發布 | 18K 次閱讀 游戲 機器學習

使用函數式編程語言 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、復用都很方便無副作用。所以為什么不去試著在寫一個游戲呢。

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!