Skip to content

Instantly share code, notes, and snippets.

@terurou
Last active July 2, 2021 11:21
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save terurou/827b60987f927f67cf775142a15e524b to your computer and use it in GitHub Desktop.
Save terurou/827b60987f927f67cf775142a15e524b to your computer and use it in GitHub Desktop.
Haxe 4.0 JavaScriptターゲットでの変更点

Haxe 4.0 JavaScriptターゲットでの変更点

Haxeの次期大型アップデート Haxe 4.0 は近日リリース予定です。

この記事の執筆時点の最新開発バージョンは 4.0.0-rc.2 です。2019年5月8日にHaxeの大規模カンファレンスが開催されるため、これに合わせて 4.0.0-final がリリースされる見込みでした。しかし、一部機能の開発が収束していないため、 直近のカンファレンスでは 4.0.0-rc.3 のリリースに留まる可能性も出てきています。

とはいえ、Haxe 4.0は現状のバージョンで既に実戦投入レベルには達しています。私の周辺では、既に3プロジェクトがHaxe 4.0に移行済みで、うち1つが本番稼働中、他2つも数か月以内にはリリースを予定しています。

この記事では、 JavaScriptターゲットに関するものに絞った Haxe 4.0 の変更点 をまとめました。Haxeは JavaScript, C++, C#, PHP, Flash, …​ などマルチターゲットにコンパイルできる言語ですが、全ターゲットの情報を追うのはしんどいので、JavaScript以外のターゲットについては、皆さんの方で調べてみてください。

開発環境の改善

コンパイラサービスによる入力補完が大きく改善され、単純なコード補完だけではなく、auto-importや文脈に従ったコード生成もできるようになりました。

また、Haxe本体の開発と並行して、開発環境の整備も行われています。Haxeコミュニティ公式は VS Code の対応を重点的に進めていますが、LSP(Language Server Protocol)を実装した Haxe Language Server も同時に提供されています。LSPに対応しているエディタであれば、Haxe開発環境を構築することが容易になりました。

構文関連の変更

アロー関数(Arrow functions または Short Lambda)

以前から要望の強かったアロー関数構文が追加されました。次のように functionreturn を省略することができます。

// Haxe 3.x
array.filter(function (x) return x % 2 == 0).map(function (x) return x * 10);

// Haxe 4.0
array.filter(x -> x % 2 == 0).map(x -> x * 10);

構文は次の通りです。

// 引数なし
() -> expr

// 単一引数
a -> expr
(a) -> expr
(a: Int) -> expr

// 複数引数
(a, b) -> expr
(a: Int, b: String) -> expr

return を明示的に記述することも可能です。関数の途中で return して、末尾で return しなかった場合は、末尾の式が戻り値として扱われます。

(a: Int) -> {
    if (a < 0) return false;
    true; // return true; と等価
}

値を指定しない return を記述すると、戻り値が Void のアロー関数となります。

// Void -> Void
() -> return

引数名付きで関数型を宣言する構文の追加

次の例のように、引数名のついた形式で関数型を宣言できるようになりました。これにより、単純に型の自己文書化が高まる他、コンパイラサービスがこの情報を元にAPIヒントを表示したりコード生成したりするようにもなりました。

// Haxe 3.xまでの構文(Haxe 4.0以降も引き続き使用可能です)
String -> Int -> Void

// Haxe 4.0で追加された構文
(name: String, age: Int) -> Void

Haxeコミュニティ公式としては、新しい構文の使用を推奨しています。

予約語の追加

finaloperatoroverload が予約語に追加されました。

final 変数

var x 構文の代わりに final x 構文で変数を宣言すると、イミュータブル変数が定義できるようになりました。Haxe 3.xまではinlineやリードオンリーなプロパティで部分的には代用はできていましたが、この構文により、ローカルレベルの変数もイミュータブルにできるようになりました。

final 変数は原則として宣言時のみ代入(初期化)が可能ですが、static ではないフィールドレベルのイミュータブル変数に限り、コンストラクタでの代入も可能です。

class Foo {
    static final foo = 10;
    final bar = 100;
    final baz: String;

    public function new(baz: String) {
        this.baz = baz;
    }

    public function toString(): String {
        final val = foo + bar;
        return `${baz}:${val}`;
    }
}

Haxe 3.x以前からの古い構文 var x(default, never) で定義したプロパティと、 final x 構文で定義したフィールドは同じものとして扱われます。ただし、古い構文はHaxe 4.1以降で非推奨となる(もしくは削除される)可能性があるため、Haxe 4.0以降は原則として final を使うことが推奨されます。

また、Haxe 4.0のコンパイラは、次の記述をどちらも同じものとして扱います。

static inline var x = 10;
static inline final x = 10;

Empty Mapリテラル

[] リテラルをMap型と推論される箇所で記述した場合、空のMapと解釈されるように改善されました。

final map:Map<Int, String> = [];

key-valueイテレーター構文

Map などの key-value を持つデータに対し、keyとvalueを同時に取得できるイテレーター構文 for (key ⇒ value in e) が追加されました。

var map = [ "foo" => 10, "bar" => 20 ];

for (key => value in map) {
    trace(key, value);
}

この構文は、構造的部分型 KeyValueIterator<K, V> を満たしているオブジェクトに対して使用可能です。上記の例では、 Map<K, V>KeyValueIterator<K, V> を満たしています。

for (i in 0…​X) 構文がループ展開されるように

for に対してIntIterator 0…​5 や配列リテラル [1, 2, 3] などのコンパイル時点でループ回数が判定できる値を指定した場合、ループ展開(loop-unrolling)されるようになりました。

// Haxe
for (i in 0...5) {
    trace(i);
}
// JavaScript
console.log("Main.hx:4:",0);
console.log("Main.hx:4:",1);
console.log("Main.hx:4:",2);
console.log("Main.hx:4:",3);
console.log("Main.hx:4:",4);

null-safety(実験的機能)

あくまでも実験的機能ですが、 null-safety(null安全性チェック)機能が追加されました。

コンパイラフラグで --macro nullSafety("my.pack") を対象とするパッケージを指定する方法か、classに対して @nullSafety メタデータを指定する方法のどちらかで、この機能を有効にできます。nullチェックはクラスと抽象型を対象にできます。(注:4.0.0-rc.2の時点ではクラスのみにしかnullチェックが行われていませんでしたが、最新開発バージョンでは抽象型に対してもnullチェックが行われるように改善されています。)

この機能を有効にしている場合、明示的に Null<T> 型として定義されている箇所以外では、 null を代入することが不可能になり、コンパイルエラーが出力されるようになります。

var a: Null<Int> = null;  // no error
var b = null;             // Null safety: Cannot assign nullable value here.

Null<T> 型の値を参照する時は、nullチェックが必須になります。

function exsample(foo: Null<String>, bar: Null<String>): Void {
    // no error
    if (foo != null) {
        var f = foo;
    }

    // Null safety: Cannot assign nullable value here.
    var b = bar;
}

また、--macro nullSafety("my.pack", Loose)@nullSafety(Off) のように、Strict, Loose, Off の3段階のチェックレベルを指定できます。省略時の既定値は Loose です。 Off を指定するとnullチェックは行われません。

StrictLoose の差異は、次のようなコードで現れます。この例では、nullチェックのifの内側で mutate() を呼び出していますが、mutate() により null が代入されています。

function example(o: {field: Null<String>}) {
    if (o.field != null) {
        mutate(o);
        var notNullable: String = o.field;  // Strictではエラー、Looseではエラーとならない
    }
}

function mutate(o: {field: Null<String>}) {
    o.field = null;
}

null との比較が最適化

var x = null; if ("foo" == x) { } のような比較が最適化され、効率的に処理されるようになりました。

Auto-"using" for types

@:using メタデータにより型デコレーションができるようになりました。 Static Extension と類似する機能です。

// Maybe.hx
@:using(MaybeTools)
enum Maybe<T> {
    Just(x: T);
    Nothing;
}
// MaybeTools.hx
class MaybeTools {
    public static function isJust<T>(a: Maybe<T>): Bool {
        return switch (a) {
            case Just(_): true;
            case Nothing: false;
        }
    }
}
final x = Maybe.Nothing;
trace(x.isJust()); // false

Haxe 4.0.0-rc.2の時点では、classとenumに対してのみ指定可能ですが、4.0.0-finalではinterfaceとabstractに対しても指定可能になりそうです。

匿名構造体型(Anonymous Structure)のフィールド構文の変更

イミュータブルフィールドを定義する final x と、オプショナルフィールドを定義する var ?x final ?x 構文が追加されました。

{
    final a: Int;

    var ?x: String;
    final ?y: Bool;
}

var x(default, never) 構文はイミュータブル変数と同様に、 final x 同義です。Haxe 4.1以降で非推奨となる(もしくは削除される)可能性があります。

var ?x final ?x は、それぞれHaxe 3.xと同様に @:optional メタデータを使って @:optional var x @:optional var x 記述することも可能ですが、将来的には @:optional メタデータは非推奨となるものと思われます(注:私の知る限りでは非推奨とするという情報は見つけられていません)。

交差型(Intersection Types)の追加と型パラメーター制約の構文変更

交差型とは、複数の匿名構造体型を1つの型に合成したものです。 交差型を定義する A & B 構文が追加されました。

Haxe 2.xから匿名構造体型には「継承」というほぼ同じ概念がありますが、交差型はこれを構文レベルで置き換えます。交差型構文の導入により、Haxe 4.0からは交差型構文が推奨とアナウンスされていますが、古い「継承」構文でも引き続き記述できます。

typedef Type1 = { var name: String; }
typedef Type2 = { var age: Int; }

// Haxe 4.0:交差型構文
typedef Type3 = Type1 & Type2;
typedef Type4 = Type1 & Type2 & {
    function toString(): String;
}

// Haxe 3.x:「継承」構文
typedef Type3 = {>Type1, >Type2, };
typedef Type4 = {
    >Type1,
    >Type2,
    function toString(): String;
}

また、型パラメーター制約の構文が交差型の構文と統一されました。複数の匿名構造体型を型パラメーター制約として指定する場合、 T: A & B のように記述します。旧構文 T: (A, B) はHaxe 4.0で削除されたため、Haxe 3.xから移行する際は修正が必要となります。

// Haxe 4.0
class Foo<T: Type1 & Type2> { }

// Haxe 3.x:この構文はHaxe 4.0以降では使えません
class Foo<T: (Type1, Type2)> { }

enum abstract の構文変更

enum abstract 型を定義する際、 @:enum メタデータではなく、 enum 修飾子を使って記述できるようなりました。Haxe 4.0時点では enum 修飾子と @:enum メタデータは共に使用可能ですが、将来的に @:enum メタデータは非推奨になるものと思われます(注:私の知る限りでは非推奨とするという情報は見つけられていません)。

// Haxe 4.0
enum abstract HttpStatus(Int) {
    var NotFound = 404;
    var MethodNotAllowed = 405;
}

// Haxe 3.x
@:enum abstract HttpStatus(Int) {
    var NotFound = 404;
    var MethodNotAllowed = 405;
}

enum abstract に auto-numbering と auto-stringing 機能が追加

基底型がInt型かString型の enum abstract 型に限り、値を省略して定義することが可能になりました。この場合、次のように自動的に値が設定されます。

enum abstract Enum1(Int) {
    var A;          // 0
    var B;          // 1
    var C = 1000;   // 1000
    var D;          // 1001
    var E;          // 1002
}

enum abstract Enum2(String) {
    var A;      // "A"
    var B;      // "B"
    var Hello;  //"Hello"
}

@:fakeEnum メタデータの削除

@:fakeEnum メタデータはenum abstractとほぼ同等機能を提供するレガシー機能です。レガシー扱いとなってからも長い間残されていましたが、Haxe 4.0でついに削除となりました。

enum値をデフォルト引数に指定できるように

パラメータを持たないenum値に限りますが、enum値をデフォルト引数に指定できるようになりました。

function foo(option: haxe.ds.Option<Int> = None) { /* ... */ }

関連情報

クラス、インターフェース、メソッドに対する final

クラスとインターフェースの継承禁止、メソッドのオーバーライド禁止の指定方法が、 @:final メタデータではなく、 final 修飾子を使う構文に変更なりました。Haxe 4.0時点では final 修飾子と @:final メタデータは共に使用可能ですが、将来的に @:final メタデータは非推奨になるものと思われます(注:私の知る限りでは非推奨とするという情報は見つけられていません)。

// Haxe 4.0
final class Foo { /* ... */ }
final interface Bar { /* ... */ }
class Baz {
    public final function hello(): String {
        return "hello";
    }
}

// Haxe 3.x
@:final class Foo { /* ... */ }
@:final interface Bar { /* ... */ }
class Baz {
    @:final public function hello(): String {
        return "hello";
    }
}

関連情報

switch式の変数キャプチャの構文変更

switch 式で変数キャプチャを使う場合、 casevar final を記述できるようになりました。 final を付けた場合、キャプチャした変数はイミュータブルとなります。

Haxe 3.xまでと同様に var final を記述せずに省略しても構いません。省略した場合は var と同等として扱われ、キャプチャした変数はミュータブルとなります。

final array = [1, 2, 3];
switch (array) {
    case []: trace("empty");
    case [a]: trace(a);
    case [var a, final b]: trace(a, b);
    case final x: trace(x);
}

関連情報

フィールドレベルのexternの構文変更

フィールドレベルに対してのexternの指定が @:extern メタデータではなく、 @: が不要な extern 修飾子を使って記述できるようなりました。Haxe 3.xではclassに対してのみ extern 修飾子が指定できましたが、この変更により構文が統一されました。

Haxe 4.0時点では extern 修飾子を使って記述したコードと @:extern メタデータを使って記述したコードの扱いは同じですが、将来的にメタデータを使った記述方法は非推奨になるものと思われます(注:私の知る限りでは非推奨とするという情報は見つけられていません)。

演算子オーバーロード @:op(a.b) で代入が可能に

. 演算子オーバーロードにgetterだけでなくsetterも定義できるようになり、代入を記述することが可能になりました。

abstract DotAccess<T>(Map<String,T>) {
    public function new() {
        this = new Map();
    }

    @:op(a.b) function get(field: String): T {
        return this[field];
    }

    @:op(a.b) function set(field: String, value: T): T {
        return this[field] = value;
    }
}
var d = new DotAccess();
d.hello = 5;
trace(d.hello);

Call-site inlining

inline 修飾子が付いていない関数・メソッドも、呼び出し側で inline を指定して呼び出すことで、強制的にインライン展開できるようになりました。コンストラクタに対しても指定可能ですが、プロパティやフィールドに対しては指定できません。

final p1 = inline new Point(0, 1);
final str = inline p1.toString();
trace(str);

final p2 = inline Point.fromArray([0, 1]);
class Point {
    public final x: Int;
    public final y: Int;

    public function new(x: Int, y: Int) {
        this.x = x;
        this.y = y;
    }

    public function toString(): String {
        return '[$x, $y]';
    }

    public static function fromArray(p: Array<Int>): Point {
        return new Point(p[0], p[1]);
    }
}

implements Dynamicextern class のみに制限

型安全性が壊れるという理由で、 Implementing Dynamic を使用できるのは extern class のみに制限されました。

カスタムプロパティアクセサ構文 var a(get_x, set_x) の削除

Haxe 3.xの時点で非推奨扱いになっていた古い構文が削除されました。

インタフェースでフィールドデフォルト値を設定する構文の削除

C++ターゲットでは動作していたようですが、他のターゲットでは動作しないため、構文自体が削除となりました。

inlineではないexternフィールドに対して式を記述すると警告するように

Static extensionが暗黙の型変換と同時に動作しないように挙動が変更

次のようなコードがHaxe 3.xでは動作していましたが、Haxe 4.0から動作しなくなりました。

using StringTools;

class Test {
    static function main() {
        var myInt: MyInt = 10;
        var str = myInt.rpad("_", 10); //暗黙的にtoString()してStringTools.rpad()
        trace(str);
    }
}

abstract MyInt(Int) from Int {
    @:to
    public function toString(): String {
        return Std.string(this);
    }
}
using StringTools;

class Main {
    static function main() {
        new MyString();
    }
}

abstract MyString(String) {
    public function new() {
        this = "123";
        trace(this.rpad("#", 10));  //OK: これはHaxe4.0でも動作します
        trace(rpad("#", 10));       //NG
    }
}

型ヒントも初期値も指定されていないスタティック変数は宣言不能に

static var x: String; //OK
static var y = 1;     //OK
static var z;         //NG

名前空間付きメタデータ(Namespaced metadata)

メタデータ名に . が使えるようになりました。 @:js.someName のように記述できます。

@:nativeoverride でも場合も継承されるように

@:expose class は暗黙的に @:keep が付与されているものとして扱うように

演算子オーバーロード用の @:commutative メタデータがstaticではないメソッドでも指定可能に

変数シャドーイングの警告

コンパイラフラグに -D warn-var-shadowing が追加されました。有効にすると、変数シャドーイングをしている個所に対して、コンパイラ警告が出力されます。

class Main {
    static function main() {
        var a = 1;
        var a = 2;
        trace(a);
    }
}
Main.hx:4: characters 13-14 : Warning : This variable shadows a previously declared variable
Main.hx:3: characters 13-14 : Warning : Previous variable was here

標準ライブラリ関連の変更

標準ライブラリから除外されるパッケージ

次のパッケージが標準ライブラリから削除され、 hx3compat に移されました。

  • js.jquery

    • 元々、APIサポート状況が悪く haxelib の jQueryExtern を使うべきという状況でした。

    • 時代の流れで、jQueryの重要度が下がっているという背景もあるかと思われます。

  • js.swfobject

    • SWFObjectを操作するAPIですが、流石に時代の流れを感じますね。

  • haxe.unit

    • Unit Testの実装ですが、実用には厳しい非常に素朴なAPIしか提供されておらず、実利用されていなかったと思われます。

    • 少し前に HaxeのUnitTestライブラリ(2018年版) という記事を書いたので、代替手段はこちらを参考にしてください。

  • haxe.remoting

    • HaxeでFlash/AMP RemotingやJS/Flash間の通信をサポートするパッケージですが、これも今更感があり、時代の流れですね。

  • haxe.web

    • HaxeでHTTP APサーバーを構築するための簡易的なRouterの実装です。

    • おそらくNekoターゲットを想定して作られたものと思われ、ほとんど利用されていなかったと思われます。

標準ライブラリのUnicode対応の強化

Haxeは長らくUnicode対応が不完全で、標準ライブラリのみでUnicodeを処理してはならないという残念な状況が続いていましたが、Haxe 4.0でやっと改善が行われることになりました。公式ブログではパフォーマンス上の理由や歴史的経緯が説明されていますが、個人的には単にemojiの利用が広まって逃げられなくなっただけではと認識しています。

主な改善点は次の3点です。

  • haxe.Utf8 がサロゲートペアを考慮した実装に変更

  • haxe.io.BytesofString() などでエンコーディングを指定可能に

  • String にコードポイントベースのイテレーターメソッド iterator()keyValueIterator() が追加

JavaScriptターゲットとは直接関係はしませんが、sys パッケージもファイルパスなどを扱うためにUnicode対応が進められています。

注意点として、Haxe 4.0以降も String は引き続きプラットフォーム依存で、原則としてネイティブAPIが呼び出されるだけとなります。JavaScriptターゲットでの "🐐".length の結果は 2 となりますが、Evalターゲットでの結果は 1 となります。私が確認した限り、StringToolsStringBuf などは String.length で処理を行っています。

Haxe公式ブログでも、Haxe 4.0がUnicode対応の最初の一歩で、4.1でも継続して対応を行うと言っているので、IssueやPRを出していけば以前よりも積極的に対応されると思われます。

Std.is(x, T) の仕様変更

Std.is(x, T)x == null の場合、 T にどんな型を指定しても必ず false を返すように規定されました。

catch (e: T) の仕様変更

catch (e: T)null がスローされてきた場合、必ず型アノテーションが Dynamic と指定されている catch でのみキャッチされるように規定されました。

cast(x, T) の仕様変更

cast(x, T) はJavaScriptなどの静的ではない環境で null を セーフキャスト cast(x, T) した場合は、必ず null を返すようになりました。なお、C++などの静的環境では、 cast(null, Int)0cast(null, Bool)false になるなど、環境差異が生じることに注意が必要です。

Type.enumEq() の仕様変更

abstract enumに対して Type.enumEq() を使う事ができなくなりました。

イテレーター関連の更新

  • haxe.iterators パケージの追加

  • MapkeyValueIterator() を追加

  • haxe.DynamicAccessiterator()keyValueIterator() を追加

haxe.ds.ReadOnlyArray の追加

haxe.ds.BalancedTreehaxe.Constraints.IMap を implements するように変更

Lambda の関数の戻り値が List から Array

Lambda クラスの map()concat() などの関数(スタティックメソッド)の戻り値が List から Array に変更されました。

Array#resize() の追加

EReg.escape() の追加

PHPの preg_quote 相当の処理を行います。

関連情報

haxe.Http classのメソッドが return this しないように変更

haxe.xml.Fast が非推奨に変更

haxe.xml.Access という Xml classをベースとしたAPI互換のあるabstract型が作成され、haxe.xml.Fast はそれに対するaliasに変更されました。

haxe.Log.formatOutput() の追加

trace() 内部のログフォーマット処理が haxe.Log.formatOutput() に切り出されました。

Haxeでは trace() の実体 haxe.Log.trace() が dynamic function として定義されており、この関数を上書きすると trace() の挙動を自由に変更できます。 trace() のメッセージフォーマットは変更しなくてもよいが、メッセージ出力先を変更したいケースが haxe.Log.formatOutput() の典型的な使用例となります。

JavaScriptターゲット固有の変更

ビルトインオブジェクトを js.lib パッケージ配下に統合

ObjectDatePromiseError などのJavaScript Nativeのビルトインオブジェクトが js.lib パッケージ配下に統合されます。

js.Promise などのHaxe 4.0より前から存在していた整備対象のモジュールは、互換性維持のため、 js.lib パッケージへのエイリアスとして従前のパッケージ配下にも残されます。ただし、このエイリアスは @:deprecated メタデータが付与され、使用するとコンパイル警告が出力されます。

  • js.lib.Object

  • js.lib.Errorjs.Error

    • このモジュール内に、 EvalError, RangeError, ReferenceError, SyntaxError, TypeError, TypeError も定義されています。

  • js.lib.Function

  • js.lib.Date

  • js.lib.RegExpjs.Promise

  • js.lib.Promisejs.RegExp

  • js.lib.Map

  • js.lib.Set

  • js.lib.Iterator

  • js.lib.Symbol

  • js.lib.ArrayBufferjs.html.ArrayBuffer

  • js.lib.ArrayBufferViewjs.html.ArrayBufferView

  • js.lib.DataViewjs.html.DataView

  • js.lib.Float32Arrayjs.html.Float32Array

  • js.lib.Float64Arrayjs.html.Float64Array

  • js.lib.Int16Arrayjs.html.Int16Array

  • js.lib.Int32Arrayjs.html.Int32Array

  • js.lib.Int8Arrayjs.html.Int8Array

  • js.lib.Uint16Arrayjs.html.Uint16Array

  • js.lib.Uint32Arrayjs.html.Uint32Array

  • js.lib.Uint8Arrayjs.html.Uint8Array

  • js.lib.Uint8ClampedArrayjs.html.Uint8ClampedArray

js.html パッケージの更新

HTML/DOM関連のJavaScript Web API(ブラウザAPI)が最近の状況に合わせて更新されています。ざっと確認した限りでは、getUserMedia() ServiceWorker Push API WebMIDI などが入っているようです。

js.Syntax の追加

Haxeコードから生のJavaScriptコードを注入する js.Syntax が追加されました。蛇足的ですが、同様に php.Syntaxpython.Syntax も追加されています。

以前からほぼ同等機能の untyped 関数が提供されていましたが、js.Syntax はコンパイラ・コードアナライザーに対してやさしくなっていることが大きな違いです。 untyped ではまったくコンパイラの支援が受けられませんが、js.Syntax はある程度のコンパイラ支援が受けられます。

Haxe 4.0では untyped 関数が非推奨となったため、js.Syntax を使うようにしてください。

js.Lib.getOriginalException() の追加

Haxe/JSではどのような値も例外としてスローできます。throw 1 のように js.Error 派生classではない型の値をスローするコードをコンパイルすると、Haxeコンパイラは throw new js$Boot_HaxeError(1) のように、例外用の型にラップしてからスローするJavaScriptコードを出力します。また、 try catch でスローされてきた例外の値を参照するHaxeコードは、 e instanceof js$Boot_HaxeError でラップされた値かを判定してアンラップするJavaScriptコードとして出力されます。

この挙動を気にすることは滅多にないのですが、js__$Boot_HaxeError がスローされてこないことが明白な場合(JavaScript Native側で発生する例外をキャッチする場合など)にアンラップ処理を行いたくないことは稀に発生します、そのような場合に js.Lib.getOriginalException() を使うと、アンラップ処理を行っていない例外の値を取得できます。

js.lib.Promise のコールバック引数の型を変更

Haxe 3.xまでの型定義では then() が正しく型推論できないことが度々発生していましたが、この問題が修正されました。

ES6構文出力オプション -D js-es=6 の追加

コンパイラフラグ -D js-es6 も受け付けるようになりました。-D js-es=6 を指定すると、ES6class構文を使ったJavaScriptコードが出力されます。

SourceMap出力オプション -D source-map の追加

コンパイラフラグで -D source-map を指定すると、 -debug 指定なしのリリースビルドでもSourceMapを出力するようになりました。

なお、同様のオプション -D js-source-map と PHP用の -D source_map がHaxe4.0より前のバージョンから存在しているのですが、こちらも引き続き使用可能です。

Array#iterator() の性能改善

Haxe 3.xでは Array#iterator() でIteratorに変換すると効率の悪いJavaScriptコードが生成されていましたが、Haxe 4.0で改善されました。 当然Iteratorに変換すると若干冗長なJavaScriptコードになるのですが、今時のJITが搭載されたJavaScript実行環境であれば実行速度に有意差が出ることはまずないはずです。

enumの性能改善

enumの実行時の内部表現が配列からオブジェクトに変更され、性能が改善しました。もしenumの内部表現を配列にしたい場合は、 コンパイラフラグ -D js-enums-as-arrays を指定します。

実行時メタデータ obj.interfaces 出力の抑制と Std.is() の性能改善

Haxe/JSでは、 実行時に Std.is() でインターフェース実装を判定するために、JavaScriptコード生成時に obj.interfaces メタデータを付与しています。この実行時メタデータをインターフェースを実装していて、かつ Std.is() が使われているクラスのインスタンスに対してのみ生成するように改善されました。

また、 Std.is()obj.interfaces を参照する回数が最小限となるように修正され、性能が改善しました。

Std.is() でクラスと比較する場合の性能改善

Std.is() でクラスと比較を行うHaxeコードから、 instanceof を直接使うJavaScriptコード出力されるように修正され、性能が改善しました。

JavaScriptネイティブのフィールド名がHaxeのキーワードと衝突するケースの生成コードの改善

Haxe 3.xでは、Promise#catchError() をコンパイルすると promise["catch"]() のようなブラケット記法のJavaScriptコードが出力されることが度々ありましたが、promise.catch() のようなドット記法のJavaScriptコードが出力されるように改善されました。

ループ内の switchbreak を使うケースの生成コードの改善

次のような「ループ内の switchbreak するコード」をコンパイルすると、Haxe 3.xでは throwbreak を実現するJavaScriptコードが出力されていました。このため、ブラウザの開発者コンソールではthrowでデバッガが反応して停止してしまう問題がありましたが、throw を使わないように改善されました。

// Haxe
for (i in 0...10) {
    switch (i) {
        case 0: trace(i);
        case 1: break;
        case _:
    }
}
// JS that is compiled by Haxe 3.4.7
var _g = 0;
try {
    while(_g < 10) {
        var i = _g++;
        switch(i) {
        case 0:
            console.log(i);
            break;
        case 1:
            throw "__break__";  //これが問題だった
            break;
        default:
        }
    }
} catch( e ) { if( e != "__break__" ) throw e; }

マクロ関連の変更

新たなマクロインタプリタ eval

コンパイル時マクロのインタプリタが、NekoVM neko から eval に置き換えられました。Haxeコンパイラのインタプリタ実行機能(--interp-x) も同様に eval で実行されるように変更されました。インタプリタとは言っていますが、JITコンパイラが搭載されており、高速に動作します。

Haxeはコンパイルが非常に速い事が特徴の一つですが、Haxe 3.xまではマクロの実行だけは遅いという問題がありました。NekoVM自体はそれほど遅いわけではないのですが、マクロ実行に最適化されているevalに置き換えたことで速度が大幅に改善されています。

eval への変更に合わせ、一部のマクロで修正が必要になります。Haxe 3.xまではマクロが neko で実行されていたため neko ターゲット向けのコードを呼び出すことが可能でしたが、Haxe 4.0のマクロでは eval で実行されるため neko ターゲットのコードは呼び出すことができません。そのため、当該箇所については neko から eval への移行が必要となります。

また、eval の開発用に、コンパイラフラグ -D eval-stack -D eval-times -D eval-debugger が追加されました。詳細は次のURLを参照してください。

インラインマークアップ構文

XMLライクなマークアップ文字列を直接Haxeコード内に記述できるようになりました。ただし、この構文はマクロでしか使用できません。マクロでインラインマークアップを処理する際、 @:markup "<tag />" と等価のASTとして扱われます。

class Main {
    static function main() {
        final markup = <div>
            <p>hello</p>
        </div>;

        final dom = xml(markup);
        trace(dom);
    }

    static macro function xml(expr) {
        return switch (expr.expr) {
            case EMeta({name: ":markup"}, {expr: EConst(CString(s))}):
                macro $v{"XML: " + s};
            case _:
                throw new haxe.macro.Expr.Error("マークアップ構文ではありません", expr.pos);
        }
    }
}

この構文は、Haxeのスポンサー企業(Massive Interactive)および中心的な開発者が Haxe/JS + React.js で開発を行っており、シームレスにJSXを使いたいというモチベーションから導入に至ったようです(一応、公式のプロポーザルでは、JSX以外にもXMLベースの定義情報は沢山あるから欲しいよねという書き方がされています)。

Null<T> が core-type abstract に変更

Null<T>typedef Null<T> = T から @:coreType abstract に変更されました。

以前から Null<T> 型は特別扱い をされていたため、普通にHaxeコードを書く分には特に違いを感じる部分はないはずですが、ASTの型が変更されたため、マクロに影響があります。

macro-in-macro の廃止

マクロからマクロの呼び出しができなくなりました。

マクロでのstatic変数の扱いの変更

マクロ内で定義されたstatic変数は、コンパイルサーバーであっても必ずコンパイル毎に初期化するように変更されました。もしこの挙動に不都合がある場合は、static変数に対して @:persistent メタデータを付与すると、コンパイル毎の初期化は抑制されます。

haxe.macro.ContextregisterModuleReuseCall onMacroContextReused が非推奨に

前述のマクロでのstatic変数の扱いが変更されたことにより、存在意義を失ったAPIが非推奨となりました。

Typed AST node haxe.macro.TypedExprDefTEnumIndexTIdent が追加

コンパイラフラグ use-rtti-doc を削除

Haxe 3.xでマクロからドキュメントコメントの参照するには、コンパイラオプションで -D use-rtti-doc の指定が必要でした。Haxe 4.0のコンパイルサーバーはドキュメントコメントを常に保持するようになったため、このオプションは存在意義を失い、削除されました。

. を含むコンパイラフラグを受け付けるように

-D a.b のように . を含む値もコンパイラフラグとして受け付けるようになりました。また、 . を含むコンパイラフラグを条件付きコンパイルに指定する場合は、 #if (a.b) のように記述します。

コンパイラフラグ target.xxx の追加

自動付与されるコンパイラフラグ target.nametarget.statictarget.systarget.utf16target.threaded が追加され、コンパイル時に参照できるコンパイルターゲット情報が詳細化されました。

コンパイラフラグ haxe4 の追加

自動付与されるコンパイラフラグ haxe4 が追加されました。なお、Haxe 4.0でも haxe3 フラグが依然として有効となっていることに注意が必要です。

関連情報

Threadsys.thread パッケージ配下に統合

今までは、neko.vm.Threadjava.vm.Thread など、各ターゲットごとにThread APIが定義されていましたが、 sys.thread パッケージ配下に統合されました。これは本来的には標準ライブラリの話題ですが、JavaScriptターゲットでは元々Threadはサポートされておらず、使いどころはマクロのみとなるため、この記事ではマクロの話題として扱っています。

その他の変更点

Haxeコンパイラ自体のUnicode対応

標準ライブラリでUnicode対応の強化が進められていますが、Haxeコンパイラ自体のUnicode対応も行われました。

Haxeコンパイラの字句解析器がUTF-8で処理を行うように変更されました。Haxe 4.0では、この変更を足掛かりとして、入力補完サービスなどのコンパイラの機能改善が図られています。

入力補完以外の目に見える変更点としては、コンパイラ内部で扱うカラム位置がバイトオフセット(0オリジン)ではなく文字オフセット(1オリジン)に変更されました。この変更により、コンパイラサービス(入力補完やコンパイルエラー等)が指すコード位置が、文字オフセットベースに変わりました。

また、コンパイラフラグに -D old-error-format が追加されました。このオプションを有効にすると、Haxe 3.xまでと同様の動作をしますが、高度な入力補完機能が動作しなくなってしまうため、余程のことがない限りは使う事はないものと思います。

コンパイルエラーメッセージの改善

いくつかのパターンのコンパイルエラーが分かりやすいメッセージに改善されました。

コンパイラのメッセージが全てstderrに出力されるように

コンパイラフラグ --gen-hx-classes-D gen-hx-classes に変更

おまけ:Haxe 4.0には入らなかったもの

モジュールレベル関数

クラスを書かずにいきなり関数が書けるようになる言語仕様です。プロポーザルは既に通っており、Haxe 4.1で導入される予定です。

匿名構造体型のフィールドに対する @:native

Haxeでexternを定義する際にネイティブ側の識別子がHaxeの予約語と衝突している場合、 @:native メタデータを使ってHaxe側の識別子は別名にして問題を回避します。しかし現状は、匿名構造体型に対して @:native を指定しても無視されます。

typedef Parameter = {
    @:native("default")
    var defaultValue: Int;
}

Issueとしては上がっていますが、今のところ対応予定がありません。

_ 引数を無視する

Haskell, Scala, C#などの言語では既に当たり前の話ですが、関数やメソッドの引数名として _ を指定すると無視することができます。

Haxeでも _ を引数名に指定することは可能ですが、今のところ無視はされずに参照可能となっています。これでも実害は小さいのですが、、Haxe 4.0から for のループ変数に指定した _ は無視されるようになったため、引数もこの挙動に合わせてほしいところです。

@RealyUniqueName
Copy link

現時点の実装では、classのみがこのチェックの対象になり、abstractに対してはチェックが行われないようです。

This was fixed in HaxeFoundation/haxe@73a39bd

省略時の既定値はStrictです。

Actually, the default mode of null-safety is Loose: https://github.com/HaxeFoundation/haxe/blob/development/std/haxe/macro/Compiler.hx#L397

Thank you for such a comprehensive article!

@terurou
Copy link
Author

terurou commented May 1, 2019

@RealyUniqueName Thanks for your comment! I fixed it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment