JSX 私的チュートリアル

先日 JSX がリリースされて話題を呼んでいますね。
JSX - a faster, safer, easier alternative to JavaScript

JSX の謳い文句として faster, safer, easier とありますが、要は JavaScript で涙ぐましいチューニングをしなくても、それ相当、あるいはそれ以上の速度で実行できる JavaScript が生成され (faster)、静的型付けでしかも型安全、さらにはコンパイル時にエラーが検出できる(safer)、そして JavaScript のようなプロトタイプ型のオブジェクト指向ではなく Java ライクなオブジェクト指向を採用しているのでわかりやすい(easier)ということかと思います。

なので、サクッと個人で JavaScript を書きたい場合には向かなくて、大規模開発や実行速度が求められる場合にありがたみがあるんでしょうね。

縁あって JSX はリリース前から試用させていただいていたので、個人的なチュートリアルを作成してみました。

インストール

Github から clone します。Node.js + npm が入っていない場合は事前にインストールしておきます。

$ git clone git://github.com/jsx/JSX.git
$ cd JSX
$ make setup  # または npm install

make setup では JSX の Web インタフェース を起動するための設定も行いますが、必要ない場合は npm install だけでも大丈夫です。

基本事項

主な組み込みの型 & クラス

型名 概要
boolean 真偽値
int 整数
number 浮動小数点数(JavaScript の number と同じ)
string 文字列
variant どの型・クラスにも成り得る
MayBeUndefined. T の型・クラス、または undefined(廃止予定)
Nullable. T の型・クラス、または null
Array. T の型・クラスの配列(T[] と表記することも可能)
Map. T の型・クラスの連想配列
Date JavaScript の Date と同様
RegExp JavaScript の RegExp と同様

特殊な型に MayBeUndefined があります。
例えば、配列 a : number[ ] に対して a[10] としても a[10] は undefined かもしれません。なので、a の型は number[ ] ですが、a[10] の型は MayBeUndefined. になり、undefined との比較ができます。

2012/7/7 追記
新たに Nullable 型が追加され、MayBeUndefined を使うと警告が出るようになりました。Nullable 型の導入によって今まで undefinedを使用することで JSX レベルでは undefined が存在しなくなります。1
基本的には今まで MayBeUndefined にしていた部分を Nullable にし、undefined との比較をしていた部分を null との比較にすれば移行できます。
レアケースと思われますが、配列で null を代入して未定義と区別した上で undefined との比較をしていた場合は length でインデックスと要素数を比較することで、Map では hasOwnProperty で判定することで移行できます。

例えば、Nullable 型が導入される前に次のように記述していたコードは・・・

var array = [null] : Object[];
if (array[0] == undefined) {
    log "array[0] is undefined!";
}
if (array[1] == undefined) {
    log "array[1] is undefined!";
}

var map = { a: null } : Map.<Object>;
if (map["a"] == undefined) {
    log 'map["a"] is undefined';
}
if (map["b"] == undefined) {
    log 'map["b"] is undefined';
}

次のように書くことで同じ結果になります。

var array = [null] : Object[];
if (array.length < 1) {
    log "array[0] is undefined!";
}
if (array.length < 2) {
    log "array[1] is undefined!";
}

var map = { a: null } : Map.<Object>;
if (!map.hasOwnProperty("a")) {
    log 'map["a"] is undefined';
}
if (!map.hasOwnProperty("b")) {
    log 'map["b"] is undefined';
}

演算子

JavaScript との相違点は次のとおりかと思います。

  • typeof は variant 型の変数にしか適用できない
    • 演算子は異なる型に適用できないので文字列の結合に使うためには数値をキャスト(as string) する必要がある
  • 論理演算(&&, ||)は論理値にしか適用できない
  • JavaScript の a = b || c; (b が真であれば b、偽であれば c を代入する)は a = b ?: c; という書き方で代用できる(b が undefined, 0, “” などの場合に c が代入される)
  • ===, !== 演算子は存在しない(同じ型同士の比較しかしないので)

if 文は真偽値しか使えない

2012/6/11 追記
a4506fb で真偽値以外も許容するようになったようです

$ git log a4506fb -1 --oneline
a4506fb accept non-boolean expressions as conditions

JavaScript と同様の記述ができます。

JSX の if 文では真偽値しか使えません。
なので、JavaScript で数値 a に対して

if (!a) {
    ...
}

のような if 文と同じ条件にしようと思うと、JSX では

if (a == 0 || a == undefined) {
    ...
}

とする必要があります。null の場合、空文字列の場合なども考慮するとさらに条件が必要になります。

配列と連想配列

配列は JavaScript と同様に使えますが、中身は同一の型でなければなりません。異なる型を利用したい場合は variant[] で宣言します。

var array1 = [1, 2];                // number の配列
var array2 = [1, "2"] : variant[];  // 数値と文字列が混在しているので variant の配列にしなければならない

連想配列も同様です。JavaScript の違いとして、ドットでアクセスすることができません。

var map1 = { a: 1, b: 2 };                    // number の Map。a にアクセスするには map1["a"] とする。map1.a ではアクセスできない。
var map2 = { a: 1, b: "2" } : Map.<variant>;  // 数値と文字列が混在しているので variant の Map にしなければならない

ただし、variant は何でも代入できるがキャストしなければほとんど何もできない型なので、次の例のように毎回キャストする必要があります。

var map = {
    a: "a",
    b: 1,
    c: [1, 2, 3, 4, 5],
    d: { a: "a", b: "b"}
} : Map.<variant>;

log "a is " + map["a"] as string;                    // a is a
log 1 + map["b"] as int;                             // 2
log (map["c"] as int[]).join(", ");                  // 1, 2, 3, 4, 5
log "a of d is " + (map["d"] as Map.<string>)["a"];  // a of d is a

variant を使うのは極力避けて、予めどのような要素が必要か決まっている場合は Map. を使うよりも独自にクラスを定義した方が良いでしょう。

Hello World!

先に例を出した方がわかりやすいと思うので Hello Wrold! の例です。JSX Tutorial からの抜粋です。

class _Main {
    static function main(args : string[]) : void {
        log "Hello, world!";
    }
}

_Main クラスの main(:string[]) : void がエントリーポイントになります。JSX では Java のように何をするにしてもクラスを定義する必要があります。

実行方法

これも JSX Tutorial からの抜粋ですが、次のように –run オプションを付けて実行します。

$ bin/jsx --run example/hello.jsx
Hello, world!

あるいは次のように –executable オプション付きで JS を生成して、その JS を実行するということもできます。

$ bin/jsx --executable --output hello.js example/hello.jsx
$ node hello.js
Hello, world!

クラスの作成

JSX では abstract class, interface, mixin が提供されていますが、基本的なクラスの定義方法と継承方法についてのみ説明します。
abstract class や interface については JSX Tutorial の Classes and Interfaces を見るのが良いと思います。mixin の定義方法・利用方法は interface とほぼ同様です。

なお、private, public, protected のようなアクセス修飾子は存在しません。

インスタンス変数・クラス変数の定義

記法は ActionScript の記法に近いです。
インスタンス変数の定義方法は次のとおりです。

var variableName : type;

例えば foo という変数が number の配列の場合は

var foo : number[];

となります。
変数が関数の場合は少し特殊で、

var foo : function(:string) : void;

のように、どんな型の引数を取る関数で、返り値が何かを指定する必要があります。引数名は指定しません。引数名や関数名は指定しなくてもかまいません。

2012/6/17 追記

引数名を指定することもできる。更に言うと関数名すら指定することができる。

とのご指摘をいただいたので修正しました。
引数名や関数名は指定する必要がありませんが、指定してもかまいません。ただし、現在のバージョンでは引数名や関数名が指定されていても、型さえ一致していればそれらとは異なる名前でも代入などが可能のようです。

変数宣言と同時に初期化を行うこともできます。初期化によって型が定まる場合は型を指定する必要がありません。
次の例では、foo は自動的に number[] となります。

var foo = [1, 2, 3, 4, 5];

メソッド内での変数宣言も同様です。クラス変数の場合は var の手前に static をつけます。

static var foo = [1, 2, 3, 4, 5];

メソッド内でこれらの変数にアクセスするには、インスタンス変数の場合は this.variableName、クラス変数の場合は ClassName.variableName とします。

インスタンスメソッド・クラスメソッドの定義

変数同様、ActionScript に近い記法です。

function([arg1:type1[, arg2:type2[, ...]) : type {
    ...
}

また、オーバーロードがサポートされているので、同じメソッド名でも引数の数や型が違えば異なるメソッドを呼ぶことも可能です。

例えば getFoo という、number の配列 foo を返すメソッドだと

function getFoo() : number[] {
    return this.foo;
}

となります。

setFoo という、foo に値をセットするメソッドだと

function setFoo(newFoo : number[]) : void {
    this.foo = newFoo;
}

となります。
クラスメソッドの場合は function の手前に static を付けます。

なお、コンストラクタは次のように constructor というメソッドを定義するような形になります。メソッドの返り値は指定しません。

function constructor([arg1:type1[, arg2:type2[, ...]) {
}

クラスの例

以上を踏まえて Foo クラスを定義すると次のようになります。

class Foo {
    var foo : number[];

    function constructor() {
        this.foo = [1, 2, 3, 4, 5];
    }

    function getFoo() : number[] {
        return this.foo;
    }

    function setFoo(newFoo : number[]) : void {
        this.foo = newFoo;
    }
}

クラス変数・クラスメソッドの場合は次のようになります。

class Foo {
    static var foo = [1, 2, 3, 4, 5];

    static function getFoo() : number[] {
        return Foo.foo;
    }

    static function setFoo(newFoo : number[]) : void {
        Foo.foo = newFoo;
    }
}

継承

サブクラスのコンストラクタの最初にスーパークラスのコンストラクタを呼ばなければなりません。その際には super の形で呼びます。明示的に呼ばなければ super() が呼ばれます。
スーパークラスのメソッドをオーバーライドする場合は必ず override 修飾子が必要です。オーバーライドしたスーパークラスのメソッドを呼ぶには super.methodName という形で呼びます。

なお、多重継承は許容されていないので、多重継承のようなことがしたい場合は interface や mixin を組み合わせることになります。interface と mixin はいくつでも利用可能です。

以上の内容を詰め込んだのが次の例です。

class Human {
    var name : string;

    function constructor() {
        this.name = "noname";
    }

    function constructor(name : string) {
        this.name = name;
    }

    function introduce() : void {
        log "My name is " + this.name + ".";
    }
}

class Programmer extends Human {
    var language : string;

    function constructor(name : string, language : string) {
        super(name);  // 必ず最初にスーパークラスのコンストラクタを呼び出す必要がある
        this.language = language;
    }

    function constructor(language : string) {
        // super() が呼び出されて this.name が "noname" になる
        this.language = language;
    }

    override function introduce() : void {
        super.introduce();  // スーパークラスのメソッドを呼び出す
        log "I use " + this.language + ".";
    }
}


class _Main {
    static function main(args : string[]) : void {
        var noname = new Programmer("JSX");
        noname.introduce();
        // My name is noname.
        // I use JSX.

        var abicky = new Programmer("arabiki", "R");
        abicky.introduce();
        // My name is arabiki.
        // I use R.
    }
}

モジュールのロード

Python ライクなモジュールのロード機構があります。
例えば次のような classes.jsx があったとします。

// classes.jsx
class A {
    static function call() {
        log "a";
    }
}

class B {
    static function call() {
        log "b";
    }
}

classes.jsx で定義されたクラスを利用するには import でロードします。相対パスは読み込み元のスクリプトからの相対パスです。

import "./classes.jsx";

class _Main {
    static function main(args : string[]) : void {
        A.call();  // a
    }
}

同名のクラスの衝突を避けたい場合は into を使うことで回避できます。

import "./classes.jsx" into classes;  // classes という変数に import

class _Main {
    static function main(args : string[]) : void {
        classes.A.call();  // a
    }
}

一部のクラスだけロードしたい場合は from を使います。into と併用もできます。

import B from "./class.jsx";  // クラス B のみインポート

class _Main {
    static function main(args : string[]) : void {
        A.call();  // compile error: local variable 'A' is not declared
    }
}

その他の Tips

ブラウザ上で JSX から生成された JavaScript を実行する

foo.jsx という次のスクリプトを利用することにします。

// foo.jsx
class Foo {
    function constructor() {
        log "foo";
    }
}

まずは次のコマンドで JavaScript のコードを生成します。

$ jsx --output foo.js foo.jsx

JavaScript 側で new Foo を実行したければ

var foo = new JSX.require("foo.jsx").Foo$();

という形で呼び出します。
Foo の後にドルマークが付いていますが、JSX ではこれによってオーバーロードを実現しています。
例えば第1引数に number の変数を持つコンストラクタであれば Foo$N のように、引数の型を表す文字が入ります。今回は引数がないので Foo$ です。

—– 2012/10/26 追記 —–
上記の方法で呼び出すと、Foo のコンストラクタ内で this を使っていた場合に this が JSX.require(“foo.jsx”) を指すことになるので不具合があります。
よって、次のように呼び出す方が望ましいです。

var Foo = JSX.require("foo.jsx").Foo$;
var foo = new Foo();

次のような HTML ファイルを作成してアクセスすると、コンソールログに foo という文字が表示されます。

<html>
<script type="text/javascript" src="foo.js"></script>
<script type="text/javascript">
window.onload = function() {
    new JSX.require("foo.jsx").Foo$();
};
</script>
</html>

JavaScript の変数にアクセスする

js.global という Map オブジェクトを使うと JavaScript のグローバル変数にアクセスすることができます。
例えば console.warn を使いたい場合は次のようになります。

import "js.jsx";

class _Main {
    static function main(args : string[]) : void {
        var console = js.global["console"] as Map.<function(:string) : void>;
        console["warn"]("test");
    }
}

以上、簡単かつ極めて個人的なチュートリアルでした。
書き方でわからないことがあれば JSX/example, JSX/t, JSX/lib/js あたりを grep してみるといいと思います!

  1. 変換した JavaScript を実行すると undefined が出てくることもありますが、null との同値比較に == が使われるようになったので undefined との比較は true になります