2014-01-25

Haxeのenumのすごさをみんなに伝えたい

ブログもリニューアルして最初の記事くらいは技術ブログっぽい記事を書こうかと思い、今回は、Haxe(ヘックス)のenum(エニュム)についてちゃんと説明します。
Haxeそのものの紹介はこちらを見てください。
altJS勉強会「Haxeすごいからみんな使え!」

最近Haxeについて記事や紹介も色々上がってきてて嬉しい限りです。
これで、なぜか無い日本語書籍が出れば、もうちょっと弾みがつくと思うんですが、誰か英語のあれ翻訳して出版しないんですかね。
で、その一つにちょっと前に池田さんが書いたこういう記事があって、
モダンな言語でHTML5を開発しよう! 俯瞰して理解するaltJSの比較 (前篇 – TypeScript, CoffeeScript, Haxe) | HTML5Experts.jp
これに、HaxeのenumとTypeScriptのenumを一緒にすんなや!っていう文句がついてたりしました。

まあ細かいこと言っても仕方ないと思うし、同じenumなんだから「列挙型」として紹介しても仕方ないよねとは思うのですが、
実際問題としてHaxeのenumは他の言事かなり違うというか、パワーアップしているところがあるので、それが認知されていないのはもったいないなーと思いもします。

enum(列挙型)とは

他言語でenum使ってる人は飛ばして結構です。

イーナム or エナム or エニュムと読みます。僕はエニュム派。
enumerateの略です。

プルダウンメニューを想像してもらうといい感じでして、
複数の選択肢から1個だけを選ぶという場合に使う型です。

↑こんな感じですね。
Haxeで書くとこうなります

// Food.hx
enum Food
{
	Hamburg;
	Sukiyaki;
	Banana;
}

こうやって定義して

var likeFood:Food = Food.Sukiyaki;

こんな感じに使います。(詳しい記法は公式マニュアルをどうぞ)
この場合「Hamburg」「Sukiyaki」「Banana」以外の値は絶対に入りません。

1

enumの無いJSやAS3では、代わりに文字列を定数にして使っていたりしますが、
それだと文字列をそのまま代入された時とかにコンパイル時エラーが起きず、
発見が遅れたり、正しい値でも変更箇所が参照検索しにくくなります。
enumを使っていれば、それらを制限できるというわけです。

2

Haxeのenumは直和型(っぽい)

ここから、Haxeのenumの話なんですが、
Haxeのenumはなんか直和型と呼ばれるような特徴を持っています。
enumをswitch文で書くと以下のようになりますが

switch (likeFood)
{
	case Food.Hamburg :
		trace("きょうはだいすきなハンバーグよ");
	case Food.Sukiyaki:
		trace("きょうはだいすきなスキヤキよ");
	case Food.Banana:
		trace("きょうはだいすきなバナーナよ");
}

(Haxeのswitch文はbreakが無く、次のcaseまで処理は継続しません)*1

ここで、Foodの定義の方で項目を1つ増やしてやると「switch文のcase項目が足りない」とエラーが出ます。
ランタイムエラーではなく、コンパイル前の静的エラーです。これ重要。

エラーが出ると聞くと、一部のプログラマは顔をしかめ、一部のプログラマは狂喜乱舞するという噂もありますが、僕もエラーは出ないより出てくれたほうが嬉しい派で、特にこのエラーは重要です。
つまり、後で定義が増えた時に、使用しているすべてのswtchでエラーが出てくれます。
そのエラーを1つずつ対処していけばいいので、修正漏れが無くて非常に便利です。

他言語でもdefault文を使って、想定外の値の時にエラーを吐くようには出来ますが、あくまでランタイムエラーであり、実行時まで分からないし、最悪実行時にそこに処理が通らなければ気づけないこともあります。
Haxeの場合それがコンパイル前に分かるのが大きいです。
エディタが対応していれば、enumの項目を増やした瞬間気づくことができます。

enumの特徴というか、switchの特徴って気もしますが、とにかくHaxeではenumはswitchと一緒に使うことを推奨されています。

Haxeのenumは引数が持てる

ここからが多くの他言語と違ってくるところなのですが、Haxeのenumは引数を持つことができます。
コード例はこう

enum Food
{
	Hamburg;
	Sukiyaki;
	Banana;
	Etc(name:String); // ←!
}

で、使用するときはこうです

var likeFood:Food = Food.Etc("いぬまんま");

取り出すときはこう

switch (likeFood)
{
	case Food.Hamburg :
		trace("きょうはだいすきなハンバーグよ");
	case Food.Sukiyaki:
		trace("きょうはだいすきなスキヤキよ");
	case Food.Banana:
		trace("きょうはだいすきなバナーナよ");
	case Food.Etc(name): // ←!?
		trace("きょうはだいすきな" + name + "よ"); // ←!!
}

いわゆる、アンケートで、選んでくださいって項目の最後に「その他」ってあって、
「その他を選んだ人はこちらに具体的にお書きください」みたいな項目が実装できるというわけです。
引数はswitch内で引き出すことができ、nameという変数はcase文の中だけで有効です。

集合型として使う

で、本日のメインディッシュがこれ。
上の例だといまいち引数の有用性がわからないというか、むしろ自由に入れられたらenumの特徴殺してない?みたいな感じですが、
集合型として使うと途端に強力になります。

例として、ゲームとかでユーザーの入力を受けるときのことを考えます。
操作方法は画面をクリックと、キー入力で、それを記録して、後でリプレイに使いたいという場合を想定します。
普通だとこうなります。*2

interface IInput
{
}
class KeyInput implements IInput
{
	public var keyCode:Int;
}
class ClickInput implements IInput
{
	public var x:Float;
	public var y:Float;
}

IInputは空のinterfaceでちょっと気持ち悪いですが仕方ないですね・・・。
で、解釈するコードがこうなります

function setInput(input:IInput):void
{
     if (input is KeyInput){
          // 型をKeyInputにキャスト
          var keyInput:KeyInput = KeyInput(input); 
          trace(keyInput.keyCode);
     }else{
          // 型をClickInputにキャスト
          var clickInput:ClickInput = ClickInput(input); 
          trace([clickInput.x, clickInput.y]);
     }
}

うーん、気持ち悪い。
キャストが多発するのもそうですが、isでのチェックってあまり使いたくないですよね、使用箇所を検索しにくいし、さらに継承した時にややこしくなります。
あと、気づいた人もいると思いますが、このコード、入力方式が1個増えるとランタイムエラーになります。
else のところを、else if (input is ClickInput)にしておけば、入力方式が増えてもランタイムエラーは出ませんが、
出ませんけども、バグに気づけないという、ランタイムエラーより致命的な結果を招きます。「念のため将来のエラーを握りつぶす」というの、やっちゃダメですよ?そういうコード。

で、これをenumを使って実装するとこうなります

enum Input
{
     Key(keyCode:Int);
     Click(x:Int, y:Int);
}
function setInput(input:Input):Void
{
     switch(input){
          case Input.Key(keyCode):
               trace(keyCode);
          case Input.Click(x, y):
               trace([x, y]);
     }
}

こんな感じです。スッキリしますし、型キャストが消失し、何より入力方式が増えた場合静的エラーを吐きます。
切り分け処理ではランタイムエラーが静的エラーになるのは非常に重要です。
ランタイムエラーはそこを通らないと発生しないので見逃されることがありますからね。

また、静的エラーが出るということは、入力方式を増やした場合、それを扱っている箇所が全てわかるということです。
エディタのエラー一覧から1つ1つ、最適な処理を入れていくことができます。

さらに嬉しいことに、EnumはHaxe標準のシリアライザに対応しているため、このまま入力を文字列として記録することが可能です。

ポリモーフィズムとしてのEnum

別の型を、同じものとして扱うという処理に対しては、継承(インターフェース)、prototype、部分的構造型(duck type)などがありますが、
Haxeのenumはそこに新しい選択肢を1個追加してくれます。

ただ、Haxeのenumは他とは少し考え方が特殊です。
今までの継承などが、

「ブレーキやハンドルの操作方法が同じなら、車を使える人なら誰でも使える。」

というのに対して、Haxeのenumを使った方法では

「ブレーキやハンドルが色々な形をしていても、使う側が全部の車に精通していればいいよね!」

という発想になります。
switch文に全ての処理を強制するのがそこですね。

形がバラバラなものを同じように扱う場合にすごく威力を発揮する反面、種類があまりに多かったり、頻繁に追加されるような場合には向きません。
共通部分がそもそもあるのに無理にHaxeのenumを使う必要は無いため、使用しすぎないように注意しましょう。
メンバ変数や引数を使って、enumとclassの組み合わせることは、良い解になるかもしれません。
enumを使ったインスタンスのラッパーもなかなかに便利です。
(参考資料)

Ceylonについて

最近話題のCeylonの持つ、Union typeとHaxeのenumを使った集合型はちょっと似ているそうです。
僕はCeylonをあまり試していないのですが、違いとしては
Ceylonのほうが宣言や使用が即時にでき、記述が容易で、
デフォルトでダックタイプのような挙動をするのに対し、
Haxeのほうが、全ての型に対する処理を強制することが出来て硬さが高い、
といったところでしょうか。
ただし、Haxeはenum型にnullを入れることが出来てしまうので(switch文でエラーとなりますが)、その点は緩さがあります。
JSやASへ変換するときのオーバーヘッドの問題でしょうけども、ちょっと残念というかCeylonが羨ましい点です。
とはいえ、それでもHaxeのOption型は強力なので一度お試しあれ。

実際にenumを使う例はここに色々まとまっています。
Haxe白魔法使い入門、基本編。Enumの実用パターン集。 – Qiita [キータ]

  1. 省略やまとめるべき部分がありますが、わかりやすさのために書いています。 []
  2. どの言語的にも当てはまらない部分がありますが、わかりやすいように省略しています。 []
  • コメント
  • 0
  • トラックバック
  • 0

コメントする

ブログトップへ