Elmでロケット着地ゲームを作ってみた

読み時間の目安:10分以内

JavaScriptで強固なコードを書くのは大変なことです。なぜならJavaScriptには、整合性のある強力な型システムなど、昨今のプログラマが期待するような多くのツールがないからです。

そんなわけで、私たちはJavaScriptにコンパイルするための、いくつもの新しい方法を研究しています。この記事では、Elmに注目したいと思います。この言語の雰囲気をつかむために、簡単なゲームを実装してみました。デモで遊んでみてください(デスクトップのみです)

しかし、なぜJavaScriptは難しいのでしょうか? 次のようなケースを考えてみましょう。

> "hi" * 0
NaN
> "2" * 0
0
> "2" + 0
"20"
> null - undefined
NaN

有名なWatの動画では、Rubyについて簡単に触れた後に、より多くの例を挙げています。

もちろん問題点に気付いた人々は、これを解決するためにJavaScriptにトランスコンパイルする新しい言語の開発を始めました。その中でよく知られているものは、CoffeScriptTypeScript、それからDartなどです。

私は十分な時間をかけ、これら全てを使ってプロダクションコードを書きました。3つともJavaScriptという低い障壁を超える言語だと思いますが、私をワクワクさせるものではありませんでした。

例えば、TypeScriptを見てみましょう。これはインターフェースの定義の方法が面倒くさく、型安全性は十分な量のコードが型アノテーションされた場合にのみ役に立ちます。まるでフェーズチェンジのようです。特定の閾値以下では、アノテーションはほとんど役に立たないので、全てのアノテーションを省略する人もいるでしょう。ありがたいことに、型アノテーションの大部分は、すでに開発されています

次のスニペットは、any型の引数を持つ中間関数が原因で起こったプロダクションバグを簡略化したものです。

function mul(a: number, b: number) {
  return a * b;
}

function anymul(a: any, b: any) {
  return mul(a, b);
}

// The following doesn't compile and that's great.
console.log(mul([], 0)); // Breaks!

// This does compile though:
console.log(anymul([], 0));

ここ数年で、JavaScriptへのコンパイルの分野に参入するものは、ますます増えてきました。そして新たに加わったものの多くは、純粋関数型のHaskellのような言語に基づいているか、または強く型付けされた言語を直接コンパイルします。PureScriptjs_of_ocamlGHCJSElmFayClojureScriptScala.jsなどがそうです。

Elm入門

Elmは、静的に型付けされた関数型の言語であり、不変データ構造を持ち、関数型リアクティブプログラミング(FRP)への第一級のサポートを誇ります。

また、Elmには、Reactのスピーディな作成で有名な仮想DOMの実装などの、役立つツールを含む標準ライブラリがあります。
Haskellと同様に、型アノテーションは関数と同じ行、または関数の上の行に書くことができ、型は必ず大文字で始まります。

1 : Int
True : Bool
["ab", "cde"] : List String

mul : Int -> Int -> Int
mul a b = a * b

Elmは、mainエントリポイントで実行を開始します。

import Text

main = Text.plainText "Hello from Elm."

コードをもっと見るには、GitHubで私たちのロケット着地ゲームを参照してください。
まずは、たいていのディストリビューションで入手可能なHaskellのCabal-InstallからElmをインストールするようお勧めします。

cabal update
cabal install elm-make elm-package elm-compiler

これはチュートリアルではないので、詳細を学びたい方はElmのドキュメンテーションを参照してください。

FRP

Elmの関数型リアクティブプログラミング(FRP)は、入力(”信号”)を、その信号の種類に依ってコードに結びつけ、有向グラフをプロセス内で形成します。
Elmでは、DOMイベントを生成するものは何でも信号とすることができます。例えば、マウスの動作やキー操作などです。またElmには、一定間隔、例えば30fps(フレーム/秒)で刻まれる信号があり、これはゲームのプログラミングに役立ちます。改訂:Jason Merrillがコメントで指摘したように、fpsWhenを使えばfps更新のスイッチングが可能になります。Jasonに感謝します。

Elmの中の信号はどれも、その信号に依存するすべてのコードに伝播します。その流れは、通常、(入力→アプリケーション状態の更新→アプリケーションの再描画)というようになります。

Elmでは、アプリケーションが起動する前に、信号グラフ全体をセットアップする必要があります。これにより、コードを通して流入する入力について推測することが容易になりますが、欠点もあります。

例えば、ロケット着地のデモでは、ゲームをアニメーションするために30fpsが必要です。これを、ゲームの開始時にセットアップしなければならず、オフに切り替えることはできません。つまり、最初のヘルプ画面は、何も表示は変わらないのに、1秒間に30回レンダリングしているということです。

手軽なゲームを作成するのなら、それでも問題ないでしょう。ただ、考えてみてください。ユーザの入力を扱うアプリでは、何も起こらない長いポーズのあるイベントが1秒に数回起こることになるでしょう。もし30fpsでフェードアウトするアニメーションを追加する場合、アプリはその間ずっと30fpsで動作している必要があります。こうしたケースでは、Elmの(ポート)経由でJavaScriptを使うのが一番良い方法です。

なぜFRPのグラフがあらかじめ固定されているかについてはStrange Loop 2014のこの映像をご覧ください。

成熟

Elmは新しい言語で、注目されています。ライブラリはほとんどありません。さらに、バグも存在します。私はエラーのないJavaScriptを作れると見込んでいたのですが、コンパイルは可能でも破綻しているようなプログラムを何とか作り出しただけでした。例えば、以下のようなコードをコンパイルします。

ship = shipUpdate
           game.gravity
           (if game.ship.fuel > 0 then arrows.y == 1 else False)
           (if game.ship.fuel > 0 then arrows.x else 0)
           Ship

しかし、コンパイルすると”ship is undefined(shipが定義されていません)”というエラーがJavaScriptで発生します。最後のラインで間違ってgame.shipではなくShipを使ってしまったのです。

WebSocketとHTTPを通した外部との相互作用は機能していますが、まだ十分に練られていません。例を挙げると、ElmにはWebSocket用のエラー処理がありません。Elmの中心的な開発者であるEvan Czaplickiはpromise-APIを使った改善策をいくつか用意しているので、こうした課題は近いうちに解決されると思います。

また使ってみたい

批判的に聞こえたかもしれませんが、実際のところElmの体験は実に順調でした。Elmでは、状態に関して明確なコードが求められるので、適当にごまかすのはとても難しいのです。数時間ほど使って慣れてくると、すぐに動かすことができるゲームという形に、使い勝手の良さを感じるようになりました。

Elmは将来的に、ゲーム以外のプログラミングにも使用できるようになると信じています。しかし今はまだ、前述のpromise-APIなど足りない点がいくつかあります。

ちょうどこの記事を書き終えたところで、EvanがElmでアプリを構築する方法に関する素晴らしいチュートリアルをリリースしました。