Haxeのmacroでanonymous structureを動的に生成する
公式Wikiで解説が発見できなくて、ググりながら色々試しててたら、JSON-schema type builder prototype. · GitHubにたどり着いた。
理解できてしまえば簡単なのだが、ポイントとしては、
- Expr.ComplexType.TAnonymousを生成して、
- ComplexTypeTools.toType()でTypeに変換して、
- 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のexternとinlineを同時に書くとinlineが優先されるっぽい
HaxeのjQuery 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__() 以外にもいくつか追加されてたらしい。知らんかった。
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を前提とし、今はマルチターゲット(JavaScript、PHP、C++、C#等)にコンパイルする言語として設計されているため、どうしてもnulを扱わなければならない。
これはHaxeに限った話ではなく、F#やScalaなどでも同様の問題があるのだが、これらの言語と比較すると、Haxeはすこしnullが全面に出てしまっている(むしろnullを許容する言語設計にすることで、学習時の敷居を下げているのではないかとも思えるが…)ので、nullを根絶したい勢にすると、ちょっとぐぬぬとなる面がある。
少し前置きが長くなったが、Haxeで仕方なくnullを付き合うための方法の1つを書いてみる。
そもそも、nullを使わないコードとは?
Option型(言語によってはMaybe型)を使うコードである。Option型とは、「値が存在しない可能性があるデータ」を表現するための型である。
最近普及期に入ってきた型システムがリッチな言語(Scala、F#、OCaml、Haskell等)では当たり前に利用できる。
要は「nullの代わりに使いましょう」という型なのだが、nullのと大きな違いは、型安全である(「いわゆるnullチェック」にあたるコードを書かないとコンパイルすら通らない)という利点がある。nullを許容する場合だと、nullチェックを書かなくてもコンパイルが通ってしまうため、ケアレスミスによるバグが混在してしまう可能性がある。
蛇足ではあるが、リッチな型システムを使うメリットは「計算機がチェックできる範囲を広げることで、人間が楽をできる」ことであるので、食わず嫌いをして使わないのは勿体がない。
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()等のメソッドを追加すると更に便利である。本当はこのあたりを全て標準ライブラリで用意してもらいたい…。