読者です 読者をやめる 読者になる 読者になる

ジンジャー研究室

長めのつぶやき。難しいことは書きません。

HaxeとTypeScriptを両方使ってみた感想

型付きAltJSとして名高いHaxeとTypeScriptですが、両方使ってみたので比較しながら感想でも書こうかなということで。

あくまで自分の使ってみた範囲でしか話せないのでご容赦を。

執筆時点での両者のバージョンは、Haxe2.1/TypeScript0.8で、もうすぐHaxe3.0/TypeScript0.9が出ようというところ。

JavaScriptの標準的な関数の使用

当然ながらTypeScriptの圧勝。
TypeScriptはJavaScriptの拡張として実装されているので、コピペすれば大体動く。
HaxeはJavaScriptだけでなく、ActionScriptJavaC#などもターゲットにしているため、JavaScriptで普段やっていることをやるためには外部クラスをインポートする必要がある。

// Haxe
import js.Lib;
...
Lib.alert('hoge');

ただ使っていて思ったのだが、どうも動きがいつもと違う。例えばalertの場合、オブジェクトを渡すと中身をリテラルにして表示してくれたりして、それはそれで嬉しいんだけどいつものalertじゃないのか…みたいな気持ち悪さは残る。
ちなみにこのalertは引数を省略すると型チェックによって怒られる。まぁ何もメッセージを表示しないalertがそもそもおかしいと言われればそうかも知れないが。


js.Libのほかには、js.Window等がある。
HTML5の諸APIに関してはどこまで対応していたか覚えていないが、そんなに期待できない。


一方、TypeScriptの場合、console.logやalertのような関数は何の予告もなく使える。

// TypeScript
alert('hoge');


関数ではないが、if文の扱いもHaxeとTypeScriptでは異なる。
HaxeのifはBool型しか受け付けないため、存在判定はnullとの比較が必須になる。JavaScriptからHaxeに移行するとifの度にコードが膨れ上がってしまう。
同じく、&&と||もBool型でないと使えない。これは正直とても不便だ。


外部モジュールのインポート

こちらはHaxeの方が扱いやすかった。
ほぼJavaと同じ感覚で使えるのが嬉しい。

// Haxe
import hoge.Hello;
...
new Hello().world();

TypeScriptの場合、インポートしたモジュール名をクラスの頭につけて記述する必要がある。

// TypeScript
/// <reference path="hoge.ts"/>
new hoge.Hello().world();

依存性解決

上の話と絡むのだが、こちらもHaxeの方が扱いやすかった。
mainとなるクラスを指定すれば、コンパイラが依存関係を自動解決してひとつのjsファイルを生成してくれる。

haxe -main App -js app.js

一方、TypeScriptは次のようにコンパイル対象を列挙する必要がある。

tsc models.ts views.ts

コンパイル後、それぞれが同じ名前のjsファイルに変換される。


TypeScriptのモジュール機構は複雑で、慣れるまでに何度も落とし穴に嵌った
例えば、外部ファイルhoge.tsにhogeというモジュールを定義する時に

// TypeScript
module hoge {
    export class Hello {
        ...
    }
}

こんな感じでモジュールを定義するのだが、実はこのmoduleは書かなくても良くて、書かなかった場合は動的モジュールという扱いになる。
で、動的モジュールにした場合は、import文で名前を与えてやる必要がある。

// TypeScript
import hoge = module('hoge')// 右はファイル名

ここで、コンパイル対称に動的モジュールを含めた場合、ひとつの.jsファイルにコンパイルすることが出来ないという制約が付く。("Cannot compile dynamic modules when emitting into single file"とコンパイラに怒られる)
今考えればそうかも知れないと思うのだが、如何せん始めはimportを使う方が普通のやり方だと思っていたので納得がいかなかったわけで。


ちなみにこの動的モジュールは主にAMD(require.jsのような非同期読込み)に対応するものらしい。
次のようにコンパイルすると、require.jsに対応した.jsファイルを吐き出してくれる。

tsc --module amd models.ts views.ts

ううむ、なんというか言語仕様がJavaScript文化に引っ張られた感が強い。どっちが良いのかはよく分かっていない。


jQueryの利用

こちらはTypeScriptの方が扱いやすかった。


HaxeにはJQuery型が用意されているのだが、これがあり難いのか迷惑なのか良く分からない存在なのだ。

まずバージョンが古い。
確か1.6系だった気がする(haxelibに新しいのがあったような気がしなくもないが試していない)。1.6というとonが使えない。静的型付け言語なので当然コンパイラに怒られる。
TypeScriptにも共通して言えることだが、型検査を甘くするための抜け道は用意されているので、untypedをつければなんとかなるのだが、なぜ機能をオフにするためにわざわざ冗長にしているのかという気分になってくる。

(23:45 追記)
↑指摘を頂きました。jQueryExternを使えば1.9や2.0が使えるそうです。


もっと困ったのがsubmit関数で、戻り値がvoidであるためreturn false出来ずに画面遷移してしまう。これも別途対応することとなった。


そもそも記法が冗長で、$の代わりにJQueryと書かないといけないのは苦痛だ。回避策としては次のようなヘルパ関数を用意するしかない。こちら記事より引用。

// Haxe
static inline function _( str:String ):JQuery { return untyped $( str ); }

これで一応、_を$に見立てて使うことが出来る。
ちなみに、jQueryによくある$(this)を使うにはJQuery.curという変数を利用する(上の記事参照)。

jQueryに限らず、「JavaScriptのアレはHaxeでどうやるんだっけ?」が度々発生してしまうのが困ったところだ。ここでは触れないが連想配列の扱いもかなり面倒だった。(Hashクラスがあるが、連想配列との互換がない)
もちろんuntypedなどで回避は出来るが、そのたびに記述が冗長になってしまう。


以上のような経験が既にあったので、TypeScriptのjQueryライブラリ(というか型定義)はまだ試していない。そもそもjQueryは型が無くても間違えないのでほぼ心配は無い。
コードの先頭に次のように宣言することで、それ以降普通に扱うことが出来た。(この方法はTodoMVCのサンプルに倣った)

// TypeScript
declare var $: any;

パターンマッチ

TypeScriptには無く、Haxeにしかない機能としてはパターンマッチがある。
Haxeのenumはただの列挙型ではなくて、関数型の世界で言う直和型(HaskellのMaybe、ScalaのOptionのような)に相当し、強力なパターンマッチ機能を提供する(実際に強力になるのはHaxe3.0から…)。


当初の思惑としては、これがHaxeの大きなアドバンテージになることを期待していたのだが、結局あまり使っていないのでどっちでも良いような気がしている。
Scalaみたいなことがやりたかったのだが、Option型はnullの存在によって破綻したし、Either型はJSONシリアライズできないという弱点があった。(サーバー・クライアント双方がHaxeで実装されていれば、Haxeの提供するシリアライザを使う手があるのだが、相手が普通にJavaとかだとこの手は使えない。)


なので、nullもJSONも見えなくなるくらい大規模になってくれば話は別かも知れないが、今のところはどっちでもいいと思った。


シンタックス

Haxeの方がやや冗長。
特にclassにおける変数やメソッドの宣言に差が出てくる。

// Haxe
class Hoge {
    var param1: Param
    public var param2: Param
    public function new(param1, param2) {
        this.param1 = param1;
        this.param2 = param2;
    }
    public function hoge() {
        return param1;
    }
}
// TypeScript
class Hoge {
    constructor(public param1 : Param, private param2: Param) {
    }
    hoge() {
        return this.param1
    }
}

自分はそこまで気にしていないが、気にする人は気になると思う。
ラムダに関しては両者ともまだ長い、Scalaのようににmap(_+1)などと書けると大分楽。


サーバーとクライアントの接続

Haxeの大きな特徴として多言語にコンパイルできることがある。それにはおそらく次のようなメリットがある。

  1. ひとつの言語で済むので学習コストが低い
  2. 頭を切り替えなくて済む
  3. モデル定義を共有できる

ただサーバ側をHaxeで書くのは現段階では冒険に思われる(JavaC#はまだベータ)ので、せめてデータクラスだけでも共有するのが無難な気がしている(つまり3番目)。


というわけで、ここに関してはまだHaxeを使う理由が残されていると思う。
(サーバ側にNode.jsを使っているならTypeScriptでも同じことが出来る。)


速度

Haxeの方が速いらしい。
ぶっちゃけた話をすると速度については興味が薄いので(速度が求められるものを作ってこなかった)、次の記事を参考にしていただければと。

Haxe vs TypeScript でベンチマーク対決

ただ一応、半年以上前の記事なので今どうなっているかは不明。FirefoxでTypeScriptが素のJavaScriptに比べて2倍以上遅い件は改善されたのかどうか…もし改善されていないのならちょっと無視できない。

JavaScriptの可読性

これは当然ながらTypeScriptの方が読みやすい(それが売りなので)。
ただ、そのコードをどれだけ読むかと言われたら全く読まないので自分は気にしていない。両方ともSourceMapを吐き出してくれるので、開発中はずっとそっちを見ている。


ただ、出てきたJavaScriptを直接やりとりする場面がある場合はTypeScriptが向いているので、速度とのトレードオフになるかと思う。


結論

JavaScriptターゲットで今から始めるならTypeScriptかなーという気がします。
特に、既にJavaScriptに慣れ親しんでいる人にとっては、今まで普通に出来ていたことは普通に出来て欲しいと思うので、Haxeは少々ストレスかなと。


以上であります。