Haxeのmacroでanonymous structureを動的に生成する

公式Wikiで解説が発見できなくて、ググりながら色々試しててたら、JSON-schema type builder prototype. · GitHubにたどり着いた。

理解できてしまえば簡単なのだが、ポイントとしては、

  1. Expr.ComplexType.TAnonymousを生成して、
  2. ComplexTypeTools.toType()でTypeに変換して、
  3. MacroTypeに食わせる
// MyMacro.hx
class MyMacro {
    macro public static function build(): Type {
        var t = ComplexType.TAnonymous([
            {
                name: "field",
                pos:  Context.currentPos(),
                kind: FVar(macro : Int),
                meta: []
            }
        ]);

        return ComplexTypeTools.toType(t);
    }
}
// Foo.hx
typedef Foo = MacroType<[MyMacro.build()]>;

こんなかんじで、"var field: Int;"なフィールドを持ったtypedefというかanonymous structureを生成できる。

ちなみに、typedef自体もmacroで生成したい場合は、Context.defineType()を使えばいいらしい(試してない)。Build.hx · GitHubを参照。

Haxeとはどんな言語か

厳密にいえば正しくないのだけど、ざっくりこんな感じ。

  • 型システムがまともになってマクロも使えるECMAScript4(ActionScript3)
  • 型システムがまともになってマクロも使えるけど、try-with-resourcesがないJava8
  • 型システムがまともになってマクロも使えるけど、using構文とasync構文とLinq(クエリ構文)はないC#

個人的にはC#が一番近い言語なんじゃないのと思ってます。

Haxeのexternとinlineを同時に書くとinlineが優先されるっぽい

HaxejQuery externで new JQuery("selector"); って書くのがダサいなぁと思って、いろいろ試していたときにコンパイルできたコードをメモ。externとinlineを同時に書くとinlineの方が優先されるらしい。

Haxe 3.1.3で確認。

@:native("jQuery")
extern class JQuery {
    public static inline function create(selector: String, ?context: Dynamic): JQuery {
        return untyped __js__("jQuery")(selector, context);
    }
}
var elem = JQuery.create("body");
var tag = JQuery.create("<div/>");

その他のポイントとしては、untyped __js__("jQuery")で、Functionを取得して、それをコールしていること。

で、ここまでやってみたいのだけど、そもそも「jQueryの $() が色々できてしまうこと」自体がHaxeの文化にマッチしていないので、$.find() とか $.parseHTML() を使った方がいいよねという考えに至ったので、不採用とした。

HaxeのJavaScriptターゲット用のビルトイン

Haxe 3.0から __js__() 以外にもいくつか追加されてたらしい。知らんかった。

The Haxe Magic - Haxe

untyped __js__(js : String) : Dynamic

インラインJavaScript

var console = untyped __js__("console");
untyped __js__("console.trace()");

untyped __instanceof__(obj : Dynamic, type : Dynamic) : Bool

JavaScriptの"instanceof"。@ktz_aliasさんが、標準APIのStd.is()を使えば良いのではって言ってたけど、本当にそうだと思う。

if (untyped __instanceof([], Array)) { // if ([] instanceof Array) {
}

untyped __typeof__(obj : Dynamic) : String

JavaScriptの"typeof"。Haxe 3.1から使えるようになったっぽい。

switch (untyped __typeof__(x)) { // switch (typeof(x)) {
    case "string":
    case "number":
    case "boolean"
    default:
}

untyped __strict_eq__(a : Dynamic, b : Dynamic) : Bool

JavaScriptの"==="。Haxe 3.1から使えるようになったっぽい。

if (untyped __strict_eq__(x, "hoge")) { // x === "hoge"
}

untyped __strict_neq__(a : Dynamic, b : Dynamic) : Bool

JavaScriptの"!=="。Haxe 3.1から使えるようになったっぽい。

if (untyped __strict_neq__(x, "fuga")) {  // x !== "fuga"
}

Haxeの構造的部分型(typedef)ってstaticでも使える

Haxeを使い始めて2年ぐらい経つけど、今更こういうコードが書けることに気が付いた。

typedef Foo = {
    function print(): Void;
}
class Hoge {
    public static function print() {
        trace("Hoge");
    }
}
var foo: Foo = Hoge;
foo.print();

Haxeで仕方なくnullと付き合う

Haxeは現時点で選択しうるaltJSの中では型システムが一番出来が良く、代数的データ型(Haxeではenum)が扱える点が素晴らしい。しかし、元々はFlashを前提とし、今はマルチターゲット(JavaScriptPHPC++C#等)にコンパイルする言語として設計されているため、どうしてもnulを扱わなければならない。

これはHaxeに限った話ではなく、F#やScalaなどでも同様の問題があるのだが、これらの言語と比較すると、Haxeはすこしnullが全面に出てしまっている(むしろnullを許容する言語設計にすることで、学習時の敷居を下げているのではないかとも思えるが…)ので、nullを根絶したい勢にすると、ちょっとぐぬぬとなる面がある。

少し前置きが長くなったが、Haxeで仕方なくnullを付き合うための方法の1つを書いてみる。

そもそも、nullを使わないコードとは?

Option型(言語によってはMaybe型)を使うコードである。Option型とは、「値が存在しない可能性があるデータ」を表現するための型である。

最近普及期に入ってきた型システムがリッチな言語(Scala、F#、OCamlHaskell等)では当たり前に利用できる。

要は「nullの代わりに使いましょう」という型なのだが、nullのと大きな違いは、型安全である(「いわゆるnullチェック」にあたるコードを書かないとコンパイルすら通らない)という利点がある。nullを許容する場合だと、nullチェックを書かなくてもコンパイルが通ってしまうため、ケアレスミスによるバグが混在してしまう可能性がある。

蛇足ではあるが、リッチな型システムを使うメリットは「計算機がチェックできる範囲を広げることで、人間が楽をできる」ことであるので、食わず嫌いをして使わないのは勿体がない。

HaxeのOption型

Haxe 3.0から、haxe.ds.Optionという、まさにそのものが標準ライブラリとして用意された。(ちなみにEitherはない。)

Haxeでnullを使わざるをえないところ

大きく2つある。

1. デフォルト引数

Haxeは関数やメソッドにデフォルト引数が設定できるのだが、デフォルト引数にOption型を指定することができない。そのため、どうしても次のような定義を書かざるをえないことがある。

function foo(option: String = null): Void { ... }
2. 環境依存や外部ライブラリを利用するケース

JavaScriptでDOMやjQueryなどを扱わなければならない場合に多発する。

var elem = js.Browser.document.querySelector("#hoge");
// valueにはnullが入る可能性
function setProperty(name: String, value: String): Void {
    var node = new JQuery("#hoge");
    node.prop(name, value);
}

これらの他にも、コールバック関数で受け取る値がnullというようなケースも多々ある。

ちょっとした工夫をする

まず次のようなHelperを作成する。ScalaのOption型まんまである。

class OptionHelper {
    public static function create<T>(x: T): Option<T> {
        return (x != null) ? Some(x) : None;
    }

    public static function getOrElse<T>(a: Option<T>, b: T): T {
        return switch (a) {
            case Some(x): x;
            case None: b;
        }
    }
}

これだけあれば、結構いける。どうしてもコーディング規約的なものになってしまうが、無いものは仕方がない。

function foo(rawOption: String = null): Void {
    var option = OptionHelper.create(rawOption);
    ...
}
using OptionHelper; //using mixin

function setProperty(name: String, value: Option<String>): Void {
    var node = new JQuery("#hoge");
    node.prop(name, value.getOrElse(""));
}

OptionHelperにmap()等のメソッドを追加すると更に便利である。本当はこのあたりを全て標準ライブラリで用意してもらいたい…。

IntelliJ IDEA 13のJVM設定

初期値のままだと結構IntelliJスワップするので、idea.exe.vmoptionsをこんな感じにしてある。

-server
-Xms512m
-Xmx2048m
-XX:MaxPermSize=512m
-XX:ReservedCodeCacheSize=128m
-ea
-Dsun.io.useCanonCaches=false
-Djava.net.preferIPv4Stack=true
-XX:+UseCodeCacheFlushing
-XX:+UseConcMarkSweepGC
-XX:SoftRefLRUPolicyMSPerMB=50