Haxe白魔法使い入門、基本編。Enumの実用パターン集。

  • 30
    Like
  • 0
    Comment
More than 1 year has passed since last update.

以前の投稿では、Haxeの黒魔術ことコンパイル時マクロについて紹介しました。

Haxe黒魔術使い入門とWebのセキュリティの話

マクロは、コードを難解にするという邪悪な性質を持つ一方で、時として素晴らしい効力を発揮します。

一方で、Haxeには一部の人白魔法 として紹介している機能があります。

それは、 Enum(列挙型) という機能です。HaxeのEnumはマクロとは逆に、積極的に使っていくことで、コードの保守性を高める、可読性を高める、再利用性を高めるなどのメリットをもたらしてくれます。

ただしHaxeのEnumは、JavaやC#などの他の言語のEnumとは少しだけ異なる性質をもっているので、はじめてHaxeを使う人は、その使い方がわからなくてとまどったり、十分に活用出来なかったりすると思います。

今回の記事ではそんな人のためにHaxeのEnumはどんな場面で役に立つのか、どう使えば役に立つのかを詳しく説明していきます。

Enumってなんだ?

基本的な使い方については以下の公式サイト(haxe.org)にのっています。

列挙型(enum)

Enumは、「○○には××と△△と□□があるよ」ってことを表すために使います。この点は他の言語も同じですが、HaxeのEnumには 各値がそれぞれ異なるパラメータを持つことができる という特徴があります。

この特徴は本当に強力で魅力的なものですが、公式サイトに乗っているだけの内容では十分に伝わらないと思います。ですがこれから説明するEnumの実用パターン6つを知ってもらえればその価値を感じてもらえるはずです。

××と△△と□□とその他のEnum

典型的なEnumのパラメータが力を発揮する場面といえば、 ××と△△と□□とその他 という列挙をEnumを使って行いたい場合です。

例えば、文字の位置の揃え方(いわゆるalign)、一般的には、左揃え、中央揃え、右揃えの3通りがあるのが一般的です。この三通りはEnumで表すと以下のようになります。

enum TextAlign {
    LEFT;
    CENTER;
    RIGHT;
}

HaxeのEnumはパラメータを持てるため、これに その他 の文字揃えを追加することができます。

enum TextAlign {
    LEFT;
    CENTER;
    RIGHT;
    CUSTOM( positionFromLeft:Float );
}

このようにしておけば、左から60%、右から40%の位置でそろえたい、という場合に CUSTOM( 0.6 ) という値を使うことでそれを表現できるわけです。

Enumのパラメータ付いてくるというのは他の言語に慣れている人から見れば物めずらしいものかもしれませんが、実に合理的な考えがあってこのような言語仕様になっているわけなのです。

線分の当たり判定のEnum

「2つの線分の当たり判定をとりたいんだ!」ってことみなさんありますよね?

はい、ありますね。でも、線分の当たり判定を取るプログラムの設計ってちょっとめんどくさいです。

例えば、2つの線分がもつ以下の7種類の位置関係すべて対して、異なる処理をしたい場合です。

  • 2つの線分が同一である。
  • 2つの線分の一部と全部が重なっている。
  • 2つの線分の一部と一部が重なっている。
  • 2つの線分が端点と端点が接している。
  • 2つの線分の端点と線が接している。
  • 2つの線分が交差している。
  • 2つの線分は重なってないし、接してもいないし、交差もしてもいない。

そして、さらに以下の5つの場合では、より詳しい情報を取得したいです。

  • 2つの線分の一部と全部が重なっている場合、覆われているのはどちらの線分なのか?
  • 2つの線分の一部と一部が重なっている場合、重なっている範囲はどこなのか?
  • 2つの線分が端点と端点が接している場合、接しているのはどの点なのか?
  • 2つの線分の端点と線が接している場合、接しているのはどの点なのか?
  • 2つの線分が交差している場合、交点の座標はどこか?

さて、このようなプログラムをJavaで書くのを考えた場合、以下のような設計が考えられます。

位置関係の判定と、判定後の処理を1つの関数でやってしまう

× 線分の位置関係の判定は汎用な処理のはずだが、この方法では再利用できない。

位置関係の判定と、交点などの情報を取りに行く処理を別々の関数にする

× 位置関係の判定をおこなう関数と交点の座標を計算する関数などを別々にすると、同じ計算を2重にすることになる

線分の位置関係の判定のためのクラスを1つ作る

× 7つの場合それぞれで持つべき詳細情報が異なるので、1つのクラスに情報をまとめずらい。7つの場合それぞれに対してクラスを定義するのも面倒である。


どの方法をとっても、モヤモヤ感の残る設計となってしまいますが、Haxeを使うのであればこの問題は綺麗に解決することが出来ます。まず、線分の7種類の位置関係を表す以下のようなEnumを作ってしまいます。

LinesRelation.hx
enum LinesRelation {
    SAME;
    COVER( isFirstLineCovered:Bool );
    OVERLAP( startPoint:Point, endPoint:Point );
    TOUCH_EACH_OTHER( point:Point );
    TOUCH( point:Point );
    INTERSECT( point:Point );
    NOTHING;
}

そして、当たり判定の関数の返り値としてEnumを使います。

GeomTools.hx
class GeomTools {
    static public function hitTestLines(line1:Line, line2:Line):LinesRelation {
        //線分の当たり判定を行って、LinesRelationを返す。
    }
}

使うときはswitch文で条件分岐を行います。

Main.hx
class Main {
    static function main() {
        var line1 = {
            start : { x : 100.0, y :100.0 },
            end : { x : 50.0, y :50.0 },
        }
        var line2 = {
            start : { x : 50.0, y :100.0 },
            end : { x : 100.0, y :50.0 },
        }

        switch( GeomTools.hitTestLines( line1, line2 ) ){
            //それぞれの位置関係に対して処理を記述。
            case SAME:
            case COVER( isFirstCovered ):
            case OVERLAP( p1, p2 ):
            case TOUCH_EACH_OTHER( p ):
            case TOUCH( p ):
            case INTERSECT( p ):
            case NOTHING:
        }
    }
}

綺麗に、線分の位置関係の判定と、その位置関係に対して分岐を行った処理を分けて記述することが出来ました。

このように、判定すべき状態が複数あってそのそれぞれに対して異なる情報がほしい場合に、 関数の返り値としてEnumを渡す という手法が非常に効果的です。

こういった場面は幾何をあつかう場合に多いですが、その他の場面でもよくあるパターンなので、覚えておくと役に立つはずです。

構文解析のEnum

幾何の他にもEnumがハッキリと役に立つ場面があります。

それは 構文解析 です。構文解析とパラメータ付きEnumの相性はとても良いので、 「 構文解析をやるならEnumを使え 」と覚えておいてもほとんど問題ありません。

例えば、"5 + (2 + 1) * 3"という文字列を受け取って、四則演算を正しく解釈して計算したい場合、以下のようなEnumを定義してしまいます。

enum Expression {
    VALUE( value:Float );
    ADD( expr1:Expression, expr2:Expression ); //足し算
    SUBTRACT( expr1:Expression, expr2:Expression ); //引き算
    PRODUCT( expr1:Expression, expr2:Expression ); //掛け算
    DIVIDE( expr1:Expression, expr2:Expression ); //割り算
}

こうすることによって、四則演算の式をEnumの値として表現できるようになります。たとえば、"5 + (2 + 1) * 3"は以下のように表せます。

SUBTRACT(VALUE(5), PRODUCT(ADD(VALUE(2), VALUE(1)), VALUE(3)));

このようなデータ構造は 構文木 とよばれるもので、構文解析というのはいわば 文字列から構文木への変換 です。

パラメータ付きのEnumを持たない言語では、構文木をどうやって表現するかに頭を悩ます必要があります。Javaなどで構文木を作ろうとすれば複雑なクラスの継承関係が生まれたりして難解なコードになりがちです。

一方で、Haxeでは構文木を非常にシンプルに表現することができるので、Java、C#、C++よりもずっと構文解析のコードは書きやすいはずです。

正多面体のEnum

子クラスの種類をEnumで制限するということ

あるクラスの子クラスをいくつか定義したのち、その子クラスをもうこれ以上追加させたくないという場合があります。

分かりやすい例は 正多面体 です。正多面体は正四面体、立方体、正八面体、正十二面体、正二十面体の5種類しか存在しません。

正多面体のクラスをつくった場合、その子クラスは5種類しか存在していないことを保証させたいです。

こういった場合もEnumが役に立ちます。

RegularPolyhedronType.hx
//正多面体の種類は以下の5つだけ。
enum RegularPolyhedronType {    
    TETRA( object:RegularTetrahedron );
    HEXA( object:RegularHexahedron );
    OCTA( object:RegularOctahedron );
    DODECA( object:RegularDodecahedron );
    ICOSA( object:RegularIcosahedron );
}

正四面体のクラスは、正多面体クラスを継承して、親のコンストラクタにたいして、正四面体であることを表す値を渡します。

RegularTetrahedron.hx
//正四面体
class RegularTetrahedron extends RegularPolyhedron{
    public function new() {
        super( RegularPolyhedronType.TETRA( this ) );
    }
}

その他の、正多面体の子クラスも同様です。
そして、正多面体クラスのコンストラクタでEnumを受けとって引数のチェックを行います。

RegularPolyhedron.hx
//正多面体のクラス
class RegularPolyhedron {

    //読み込み専用
    public var type(default, null):RegularPolyhedronType;

    //プライベートコンストラクタ。
    function new( type:RegularPolyhedronType ) {
        this.type = type;

        //typeの引数がこのオブジェクト自身になってるかを確認。
        if ( Type.enumParameters( type )[0] != this ) {
            throw "illegal regular polyhedron";
        }
    }
}

これで、RegularPolyhedronTypeのEnumで定義されているもの以外の正多面体の子クラスをつくることが出来なくなりました。つまり、RegularPolyhedronTypeを見れば、ハッキリと正多面体が5種類しか存在しないということが分かるわけです。

子クラスをEnumで制限するメリット

上記のようなEnumで子クラスを管理する手法は、以下のようなサブクラスの種類を判別しての分岐を良く使う場合に特に有効です。

PolygonTools.hx
class PolygonTools {
    static public function hoge( polygon:RegularPolyhedron ) {
        if( Std.is( polygon, Tetrahedron ) ) {
            var tetra:Tetrahedron = cast polygon;
            //なんか処理

        }else if( Std.is( polygon, Hexahedron ) ) {
            var hexa:Hexahedron = cast polygon;

        }else if( Std.is( polygon, Octahedron ) ) {

        }else if( Std.is( polygon, Dodecahedron ) ) {

        }else if( Std.is( polygon, Icosahedron ) ) {

        }
    }
}

1つ1つクラスの種類で分岐して、キャストしていくのはめんどうですが、Enumでサブクラスの管理をしていると、上記のコードを非常にシンプルに記述することが可能です。

PolygonTools.hx
class PolygonTools {
    static public function hoge( polygon:RegularPolyhedron ) {
        switch( polygon.type ) {
            case TETRA( tetra ):
                //変数tetraがTetrahedron(四面体)型なのでキャストが要らない!!
            case HEXA( hexa ) :
            case OCTA( _ ) :
            case DODECA( _ ) :
            case ICOSA( _ ) :
        }
    }
}

Enumをswitchで分岐するコードに書き換えたため、子クラスの型のオブジェクトがパラメータとしてついてきて、オブジェクトをキャストする必要が無くなりました。

もちろん、switchによる分岐なのでcaseにモレがあればコンパイル時に教えてくれます。

セーブデータのEnum

モバイル向けのゲームを作る場合に、ゲームを閉じて、再びゲームを起動したときにゲームを元の画面から遊ばせてあげたい場合があります

このような場合では、まず、ユーザーがその時にどの画面にいたのか記録する必要があります。で、さらに言うとユーザーがどのページにいたかによって、異なる形式のセーブデータを用意しておく必要があります。

これも典型的なEnumが活躍する場面ですね。

例えば、トップページ、バトルページ、バトル結果ページのあるゲームでは以下のようなEnumを定義すれば良いのです。

enum PageData {
    TOP_PAGE;
    BATTLE_PAGE( status:BattleStatus );
    BATTLE_RESULT_PAGE( status:BattleResultStatus );
}

typedef BattleStatus = {
    stageId:String, 
    playerHealth:Int, //プレーヤーHP
    enemyHealth:Int, //敵HP
    score:Int //得点
}

typedef BattleResultStatus = {
    stageId:String, 
    isWin:Bool, //勝敗
    score:Int //得点
}

さらに、Haxeには Enumを含むデータを文字列に変換 する機能があり、簡単にファイルとして保存することが可能です。

直列化(serialize)

ちなみにOpenFLのSharedObject(オブジェクトを保存するための機能)でも、haxe.Serializerで直列化がされるので、プラットフォームを気にせずにセーブデータにEnumを使うことが可能です。

「1つ戻る」のEnum

いわゆる1つ戻る(undo)、やり直す(redo)といった履歴機能の話です。

ここでは、画像編集ソフトを例にあげます。

最も簡単なundo、redoの実装方法は、ユーザーが画像に変更を加えるごとに、画像全体を記録して保存しておくことですが、この方法では必要以上に容量を食ってしまうためあまり多くの操作を記録しておくことが出来なくなります。

しかし、画像の保存の仕方にちょっとした工夫をくわえることで、使用するメモリを節約して、より長い履歴機能を実現することが出来ます。以下のEnumを見てみましょう。

EditCommandType.hx
import flash.display.BitmapData;
import flash.geom.Point;

enum EditCommandType {
    COMPLETE_EDIT( bitmapData:BitmapData ); //全体の変更
    PARTICAL_EDIT( origin:Point, bitmapData:BitmapData ); //部分的な変更
}

このようなEnumを定義して、広範囲の変更はCOMPLETE_CHANGEとして画像全体を保存しておき、部分的な変更は、PARTICAL_CHANGEとして変更のあった範囲だけの画像とその位置を保存するようにしたものを、Arrayとして持っておきます。

undoをするときには、 一番最近の画面全体の変更を見つけてきてそこから再現したい時点まで部分的な変更を適用する ことでその時の状態を再現することができます。この方法だと1回のundoの処理に時間がかかりそうに見えますが、定期的に画面全体の保存を挟むようにすることで快適な速度で画像の再現を行うことが出来ます。

さて、ここでちょっと復習です。一番最初の実用パターンとしてでてきた「 その他 」のEnumですがこの履歴機能でも役に立ちます

EditCommandType.hx
import flash.display.BitmapData;
import flash.geom.Point;

enum EditCommandType {
    COMPLETE_EDIT( bitmapData:BitmapData ); //全体の変更
    PARTICAL_EDIT( origin:Point, bitmapData:BitmapData ); //部分的な変更
    CUSTOM( command:IEditCommand ); //その他の変更
}
interface IEditCommand {
    public function edit( bitmapData:BitmapData ):Void;
}

ユーザーがBitmapDataに対するどのような変更を行ったかさえ記録していれば画像の復元が行えるので、新たにCUSTOMというコンストラクタを追加してパラメータにIEditCommandを持たせることで任意の操作を履歴として使えるようになりました。

また、CUSTOMのパラメータがtypedefや関数オブジェクトではないのは、先述の直列化を意識しているためです。 関数オブジェクトを直列化することは出来ない ので、編集用の関数はクラスのメソッドとして定義させなくてはいけません。そのため、ここではinterfaceを使用しています。

まとめ

さて、今回は

  • ××と△△と□□とその他のEnum
  • 線分の当たり判定のEnum
  • 構文解析のEnum
  • 多面体のEnum
  • セーブデータのEnum
  • 「1つ戻る」のEnum

というEnumの6つの実用パターンを紹介しました。

どれもパラメータ付きEnumの魅力がはっきりと表れているものですが、これらはEnumで出来ることの一部でしかありません。

今回紹介できなかった「ある状態のときのみ有効な変数を、その状態の時のみ設定できるようにする」というよく使う用途もありますし、「マスターデータやフォーム入力データの正当性チェック(バリデーション)ツール」、「方程式を解いたり、有理数の演算を行ったりできるような、数式処理プログラム」などを制作する場合には、本当にとてつもない破壊力を発揮してくれます。その辺の話は、また次の機会として、今日はここまで。

明日は、@nobkzさんです。よろしくおねがいします。