设为首页收藏本站

LUPA开源社区

 找回密码
 注册
文章 帖子 博客
LUPA开源社区 首页 业界资讯 技术文摘 查看内容

使用函数式编程语言ELM开发游戏

2015-2-28 15:25| 发布者: joejoe0332| 查看: 1443| 评论: 0|原作者: 21paradox-, 无若, LeoXu|来自: oschina

摘要:   这是我首次准备就有关用elm进行游戏开发的内容撰写一个系列的文章. 这是一种能编译成 html 和 javascript,以便你可以将其直接部署到web服务器上,或者打包到 nw.js中 以创建一个独立的应用或者游戏,这样的编程 ...

  这是我首次准备就有关用 elm 进行游戏开发的内容撰写一个系列的文章. 这是一种能编译成 html 和  javascript,以便你可以将其直接部署到web服务器上,或者打包到 nw.js 中 以创建一个独立的应用或者游戏,这样的编程语言. 没有多少教程可以参考,但是随着我的慢慢进步之中我已经了解到了越来越多的东西. 因为 elm 正处在积极的发展过程中,如果或者当我的文章有点过时的时候,我将会对它们进行更新. 也会慢慢纠正我在里面留下的错误 …


  函数式编程 (FP) 是一种令人惊异的边界不清的范式. 不少人对它都有很多不同的认识,而它所为我呈现的,则是一个希望能编写更加简单的可组合式代码的群体, 以避免强 耦合 并创建出易于调试的软件.


Games

  从很多编程的领域看来,游戏开发是最适合面向对象的而且游戏总是带很多状态。从表面上看,面向对象这种方法很适合。当我使用 Functional Programming的时候,我发现这种方法也很适合游戏编程,而且我也对如何解决问题很感兴趣。


  为什么我选择用Functional Programming开发游戏呢? 简单的说,我对传统的软件设计方法感到厌倦。命令式的面向对象代码对导致一下过度设计的问题,而且很不美观。


  这仅仅是我的个人观点,所以请放松,但是如果你渴望一些不同的事物,为什么不来functional的路上看看呢。 


Iteration

  看看下面最简单的js例子,对于一个数组取平方。使用"命令式"的代码你会描述一些将要发生的事情。而用functional代码(声明式编程的子集),你去描述你想要做的事情。所以"命令式"的风格中我们会定义一个临时的index变量然后创建一个循环,遍历一遍数组,然后每个value取平方。

1
2
3
4
5
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来做这件事。

1
2
3
4
5
6
7
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 路线

1
2
3
4
5
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是这样写的:

1
2
3
4
5
6
7
8
9
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 的写法:

1
2
3
4
5
6
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是最右边的参数。仍有<|是反向作用上述过程的。

1
2
3
4
-- this
1 |> add 2
-- is equivalent to this
add 2 (1)


  当有多个function被调用,我们很容易看到这个的好处。

1
2
3
4
5
6
7
-- this
1 |> add 2
  |> add 3
  |> add 4
 
-- is equivalent to this
add 4 (add 3 (add 2 (1)))


  这样做减少了需要写的括号,而且使得代码更易读,变得更像一句话:

1
2
3
4
numbers : List Int
numbers =
[1..9] |> filter isEven
   |> map square


Composition

  一个更好的解释composition的地方,将简单的functoin组合起来编写成复杂的


  在elm理我们可以将function compose起来,通过>>操作符。这样做的好处是我们不需要指定input就可以提前把function compose起来。

1
2
3
4
-- 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)。


  在这个例子中中我们检测数字是否是奇数:

1
2
3
4
squareIsOdd =
square >> isEven >> not -- `not` 是一个built-in的方法用作布尔值反转
squareIsOdd 3 == True
squareIsOdd 7 == False


  这个inputs给了squareIsOdd一个 composed的方法,每一个方法调用时都返回了一个结果,用作下一个方法的参数。


State

State是程序储存的状态,通过对象中的变量来表示。问题在于state是这样存储的,它能够允许开发者不在当前的scope下修改变量的值,这么做是存在隐患的,比如:

1
2
3
4
5
6
7
8
9
10
11
12
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是同一内容,这样避免了不必要的拷贝。

1
2
3
noop input =
    input
sameAsInput = noop { a = "b" }


  所以取setBaz这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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次人后返回原来的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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、复用都很方便无副作用。所以为什么不去试着在写一个游戏呢。




酷毙

雷人

鲜花

鸡蛋

漂亮
  • 快毕业了,没工作经验,
    找份工作好难啊?
    赶紧去人才芯片公司磨练吧!!

最新评论

关于LUPA|人才芯片工程|人才招聘|LUPA认证|LUPA教育|LUPA开源社区 ( 浙B2-20090187 浙公网安备 33010602006705号   

返回顶部