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開発環境を構築することが容易になりました。
以前から要望の強かったアロー関数構文が追加されました。次のように function
と return
を省略することができます。
// 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コミュニティ公式としては、新しい構文の使用を推奨しています。
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;
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
に対して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安全性チェック)機能が追加されました。
コンパイラフラグで --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チェックは行われません。
Strict
と Loose
の差異は、次のようなコードで現れます。この例では、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;
}
var x = null; if ("foo" == x) { }
のような比較が最適化され、効率的に処理されるようになりました。
@: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に対しても指定可能になりそうです。
イミュータブルフィールドを定義する 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
メタデータは非推奨となるものと思われます(注:私の知る限りでは非推奨とするという情報は見つけられていません)。
交差型とは、複数の匿名構造体型を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
メタデータではなく、 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;
}
基底型が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
メタデータはenum abstractとほぼ同等機能を提供するレガシー機能です。レガシー扱いとなってからも長い間残されていましたが、Haxe 4.0でついに削除となりました。
パラメータを持たないenum値に限りますが、enum値をデフォルト引数に指定できるようになりました。
function foo(option: haxe.ds.Option<Int> = None) { /* ... */ }
クラスとインターフェースの継承禁止、メソッドのオーバーライド禁止の指定方法が、 @: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
式で変数キャプチャを使う場合、 case
に var
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
修飾子を使って記述できるようなりました。Haxe 3.xではclassに対してのみ extern
修飾子が指定できましたが、この変更により構文が統一されました。
Haxe 4.0時点では extern
修飾子を使って記述したコードと @:extern
メタデータを使って記述したコードの扱いは同じですが、将来的にメタデータを使った記述方法は非推奨になるものと思われます(注:私の知る限りでは非推奨とするという情報は見つけられていません)。
.
演算子オーバーロードに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);
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]);
}
}
型安全性が壊れるという理由で、 Implementing Dynamic を使用できるのは extern class
のみに制限されました。
Haxe 3.xの時点で非推奨扱いになっていた古い構文が削除されました。
C++ターゲットでは動作していたようですが、他のターゲットでは動作しないため、構文自体が削除となりました。
次のようなコードが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
メタデータ名に .
が使えるようになりました。 @:js.someName
のように記述できます。
コンパイラフラグに -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ターゲットを想定して作られたものと思われ、ほとんど利用されていなかったと思われます。
-
Haxeは長らくUnicode対応が不完全で、標準ライブラリのみでUnicodeを処理してはならないという残念な状況が続いていましたが、Haxe 4.0でやっと改善が行われることになりました。公式ブログではパフォーマンス上の理由や歴史的経緯が説明されていますが、個人的には単にemojiの利用が広まって逃げられなくなっただけではと認識しています。
主な改善点は次の3点です。
-
haxe.Utf8
がサロゲートペアを考慮した実装に変更 -
haxe.io.Bytes
のofString()
などでエンコーディングを指定可能に -
String
にコードポイントベースのイテレーターメソッドiterator()
、keyValueIterator()
が追加
JavaScriptターゲットとは直接関係はしませんが、sys
パッケージもファイルパスなどを扱うためにUnicode対応が進められています。
注意点として、Haxe 4.0以降も String
は引き続きプラットフォーム依存で、原則としてネイティブAPIが呼び出されるだけとなります。JavaScriptターゲットでの "🐐".length
の結果は 2
となりますが、Evalターゲットでの結果は 1
となります。私が確認した限り、StringTools
や StringBuf
などは String.length
で処理を行っています。
Haxe公式ブログでも、Haxe 4.0がUnicode対応の最初の一歩で、4.1でも継続して対応を行うと言っているので、IssueやPRを出していけば以前よりも積極的に対応されると思われます。
Std.is(x, T)
は x == null
の場合、 T
にどんな型を指定しても必ず false
を返すように規定されました。
catch (e: T)
は null
がスローされてきた場合、必ず型アノテーションが Dynamic
と指定されている catch
でのみキャッチされるように規定されました。
cast(x, T)
はJavaScriptなどの静的ではない環境で null
を セーフキャスト cast(x, T)
した場合は、必ず null
を返すようになりました。なお、C++などの静的環境では、 cast(null, Int)
は 0
、cast(null, Bool)
は false
になるなど、環境差異が生じることに注意が必要です。
abstract enumに対して Type.enumEq()
を使う事ができなくなりました。
-
haxe.iterators
パケージの追加 -
Map
にkeyValueIterator()
を追加 -
haxe.DynamicAccess
にiterator()
、keyValueIterator()
を追加
Lambda
クラスの map()
や concat()
などの関数(スタティックメソッド)の戻り値が List
から Array
に変更されました。
haxe.xml.Access
という Xml
classをベースとしたAPI互換のあるabstract型が作成され、haxe.xml.Fast
はそれに対するaliasに変更されました。
trace()
内部のログフォーマット処理が haxe.Log.formatOutput()
に切り出されました。
Haxeでは trace()
の実体 haxe.Log.trace()
が dynamic function として定義されており、この関数を上書きすると trace()
の挙動を自由に変更できます。 trace()
のメッセージフォーマットは変更しなくてもよいが、メッセージ出力先を変更したいケースが haxe.Log.formatOutput()
の典型的な使用例となります。
Object
、Date
、Promise
、Error
などのJavaScript Nativeのビルトインオブジェクトが js.lib
パッケージ配下に統合されます。
js.Promise
などのHaxe 4.0より前から存在していた整備対象のモジュールは、互換性維持のため、 js.lib
パッケージへのエイリアスとして従前のパッケージ配下にも残されます。ただし、このエイリアスは @:deprecated
メタデータが付与され、使用するとコンパイル警告が出力されます。
-
js.lib.Object
-
js.lib.Error
←js.Error
-
このモジュール内に、
EvalError
,RangeError
,ReferenceError
,SyntaxError
,TypeError
,TypeError
も定義されています。
-
-
js.lib.Function
-
js.lib.Date
-
js.lib.RegExp
←js.Promise
-
js.lib.Promise
←js.RegExp
-
js.lib.Map
-
js.lib.Set
-
js.lib.Iterator
-
js.lib.Symbol
-
js.lib.ArrayBuffer
←js.html.ArrayBuffer
-
js.lib.ArrayBufferView
←js.html.ArrayBufferView
-
js.lib.DataView
←js.html.DataView
-
js.lib.Float32Array
←js.html.Float32Array
-
js.lib.Float64Array
←js.html.Float64Array
-
js.lib.Int16Array
←js.html.Int16Array
-
js.lib.Int32Array
←js.html.Int32Array
-
js.lib.Int8Array
←js.html.Int8Array
-
js.lib.Uint16Array
←js.html.Uint16Array
-
js.lib.Uint32Array
←js.html.Uint32Array
-
js.lib.Uint8Array
←js.html.Uint8Array
-
js.lib.Uint8ClampedArray
←js.html.Uint8ClampedArray
HTML/DOM関連のJavaScript Web API(ブラウザAPI)が最近の状況に合わせて更新されています。ざっと確認した限りでは、getUserMedia()
ServiceWorker
Push API
WebMIDI
などが入っているようです。
Haxeコードから生のJavaScriptコードを注入する js.Syntax
が追加されました。蛇足的ですが、同様に php.Syntax
と python.Syntax
も追加されています。
以前からほぼ同等機能の untyped
関数が提供されていましたが、js.Syntax
はコンパイラ・コードアナライザーに対してやさしくなっていることが大きな違いです。 untyped
ではまったくコンパイラの支援が受けられませんが、js.Syntax
はある程度のコンパイラ支援が受けられます。
Haxe 4.0では untyped
関数が非推奨となったため、js.Syntax
を使うようにしてください。
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()
を使うと、アンラップ処理を行っていない例外の値を取得できます。
Haxe 3.xまでの型定義では then()
が正しく型推論できないことが度々発生していましたが、この問題が修正されました。
コンパイラフラグ -D js-es
が 6
も受け付けるようになりました。-D js-es=6
を指定すると、ES6class構文を使ったJavaScriptコードが出力されます。
コンパイラフラグで -D source-map
を指定すると、 -debug
指定なしのリリースビルドでもSourceMapを出力するようになりました。
なお、同様のオプション -D js-source-map
と PHP用の -D source_map
がHaxe4.0より前のバージョンから存在しているのですが、こちらも引き続き使用可能です。
Haxe 3.xでは Array#iterator()
でIteratorに変換すると効率の悪いJavaScriptコードが生成されていましたが、Haxe 4.0で改善されました。 当然Iteratorに変換すると若干冗長なJavaScriptコードになるのですが、今時のJITが搭載されたJavaScript実行環境であれば実行速度に有意差が出ることはまずないはずです。
enumの実行時の内部表現が配列からオブジェクトに変更され、性能が改善しました。もしenumの内部表現を配列にしたい場合は、 コンパイラフラグ -D js-enums-as-arrays
を指定します。
Haxe/JSでは、 実行時に Std.is()
でインターフェース実装を判定するために、JavaScriptコード生成時に obj.interfaces
メタデータを付与しています。この実行時メタデータをインターフェースを実装していて、かつ Std.is()
が使われているクラスのインスタンスに対してのみ生成するように改善されました。
また、 Std.is()
が obj.interfaces
を参照する回数が最小限となるように修正され、性能が改善しました。
Std.is()
でクラスと比較を行うHaxeコードから、 instanceof
を直接使うJavaScriptコード出力されるように修正され、性能が改善しました。
Haxe 3.xでは、Promise#catchError()
をコンパイルすると promise["catch"]()
のようなブラケット記法のJavaScriptコードが出力されることが度々ありましたが、promise.catch()
のようなドット記法のJavaScriptコードが出力されるように改善されました。
次のような「ループ内の switch
で break
するコード」をコンパイルすると、Haxe 3.xでは throw
で break
を実現する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; }
コンパイル時マクロのインタプリタが、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>
が typedef Null<T> = T
から @:coreType
abstract に変更されました。
以前から Null<T>
型は特別扱い をされていたため、普通にHaxeコードを書く分には特に違いを感じる部分はないはずですが、ASTの型が変更されたため、マクロに影響があります。
マクロ内で定義されたstatic変数は、コンパイルサーバーであっても必ずコンパイル毎に初期化するように変更されました。もしこの挙動に不都合がある場合は、static変数に対して @:persistent
メタデータを付与すると、コンパイル毎の初期化は抑制されます。
前述のマクロでのstatic変数の扱いが変更されたことにより、存在意義を失ったAPIが非推奨となりました。
Haxe 3.xでマクロからドキュメントコメントの参照するには、コンパイラオプションで -D use-rtti-doc
の指定が必要でした。Haxe 4.0のコンパイルサーバーはドキュメントコメントを常に保持するようになったため、このオプションは存在意義を失い、削除されました。
-D a.b
のように .
を含む値もコンパイラフラグとして受け付けるようになりました。また、 .
を含むコンパイラフラグを条件付きコンパイルに指定する場合は、 #if (a.b)
のように記述します。
自動付与されるコンパイラフラグ target.name
、target.static
、target.sys
、target.utf16
、target.threaded
が追加され、コンパイル時に参照できるコンパイルターゲット情報が詳細化されました。
自動付与されるコンパイラフラグ haxe4
が追加されました。なお、Haxe 4.0でも haxe3
フラグが依然として有効となっていることに注意が必要です。
今までは、neko.vm.Thread
や java.vm.Thread
など、各ターゲットごとにThread APIが定義されていましたが、 sys.thread
パッケージ配下に統合されました。これは本来的には標準ライブラリの話題ですが、JavaScriptターゲットでは元々Threadはサポートされておらず、使いどころはマクロのみとなるため、この記事ではマクロの話題として扱っています。
標準ライブラリでUnicode対応の強化が進められていますが、Haxeコンパイラ自体のUnicode対応も行われました。
Haxeコンパイラの字句解析器がUTF-8で処理を行うように変更されました。Haxe 4.0では、この変更を足掛かりとして、入力補完サービスなどのコンパイラの機能改善が図られています。
入力補完以外の目に見える変更点としては、コンパイラ内部で扱うカラム位置がバイトオフセット(0オリジン)ではなく文字オフセット(1オリジン)に変更されました。この変更により、コンパイラサービス(入力補完やコンパイルエラー等)が指すコード位置が、文字オフセットベースに変わりました。
また、コンパイラフラグに -D old-error-format
が追加されました。このオプションを有効にすると、Haxe 3.xまでと同様の動作をしますが、高度な入力補完機能が動作しなくなってしまうため、余程のことがない限りは使う事はないものと思います。
いくつかのパターンのコンパイルエラーが分かりやすいメッセージに改善されました。
-
型推論で単一化(unification)ができずにコンパイルエラーとなる場合のエラーメッセージの改善
-
overrideフィールドが継承元classに存在しない場合のエラーメッセージの改善
-
文字列リテラルの閉じ忘れエラーメッセージの改善
-
字句解析の段階で発生するエラーメッセージの改善
クラスを書かずにいきなり関数が書けるようになる言語仕様です。プロポーザルは既に通っており、Haxe 4.1で導入される予定です。
Haxeでexternを定義する際にネイティブ側の識別子がHaxeの予約語と衝突している場合、 @:native
メタデータを使ってHaxe側の識別子は別名にして問題を回避します。しかし現状は、匿名構造体型に対して @:native
を指定しても無視されます。
typedef Parameter = {
@:native("default")
var defaultValue: Int;
}
Issueとしては上がっていますが、今のところ対応予定がありません。
Haskell, Scala, C#などの言語では既に当たり前の話ですが、関数やメソッドの引数名として _
を指定すると無視することができます。
Haxeでも _
を引数名に指定することは可能ですが、今のところ無視はされずに参照可能となっています。これでも実害は小さいのですが、、Haxe 4.0から for
のループ変数に指定した _
は無視されるようになったため、引数もこの挙動に合わせてほしいところです。
This was fixed in HaxeFoundation/haxe@73a39bd
Actually, the default mode of null-safety is
Loose
: https://github.com/HaxeFoundation/haxe/blob/development/std/haxe/macro/Compiler.hx#L397Thank you for such a comprehensive article!