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

JSX のアレな所

JSX

注意!このエントリは既に古いので、JSX の進化速度が半端ない - ぐるぐる〜もあわせて読んでください。最新のコードを参照するのが手っ取り早いです。

JSX なる言語がリリースされました。
この言語が謳っているのが、

という 3 点です。
高速と安全はまぁいいでしょう*1
問題は、はたしてこの言語は簡単なのか?という点です。
簡単かどうかは人によるのでアレなのですが、まぁ一部の人にとっては簡単とは言えない (というか書く気がしない) 書き方を強制されるのです。

関数型

数値を受け取って文字列を返す関数を表す型は、JSX では以下のように書きます。

function(:number):string

これ単体で見ると分かりやすそうな気配はします。
では、これ読めますか?

function f(g: function(:number):number): function(:number):string {
  return function(x: number): string {
    return g(x).toString();
  };
}

function という言葉に

  • 関数の定義のキーワード
  • 関数型のキーワード
  • ラムダ式を作るためのキーワード

と、3重の意味を持たせてしまっているんですよね。
JavaScript は最初と最後の 2 つだったのですが、それにもう一個増えたことになります。
これを「読みやすい!簡単だ!」という人は、ごく少数なのではないでしょうか?
ここは、既存の言語と合わせて、

number -> string

としてほしかったです。
そうすれば、

function f(g: number -> number): number -> string {
  return function(x: number): string {
    return g(x).toString();
  };
}

と、こちらの方がまだ読めるし書く気になりますよね。

ラムダ式の型推論

でも関数の定義部分で「戻り値は number 受け取って string 返す関数だ」と言っているわけですから、

function f(g: number -> number): number -> string {
  return function(x) {
    return g(x).toString();
  };
}

と書けてもいいと思いませんか?俺は思います。
このくらいであれば、関数の定義部分での型指定が増える程度なので、許容範囲です。
欲を言うと、

// この関数の戻り値の型は関数本体から推論できる
function f(g: number -> number) {
  // ここのラムダ式の戻り値の型は、toStringの戻り値がstringなので決定
  // 引数のxは、gに渡していることからnumberで決定
  // つまり、このラムダ式はnumber -> string
  // 以上のことから、この関数の戻り値の型は、number -> stringとわかる
  return function(x) {
    return g(x).toString();
  };
}

のように、戻り値の型が明白な場合は省略できると個人的には嬉しいです。
ただ、これは「ルールが多くて複雑!」と思ってしまう人もいるようなので、強くは言いません。

ループ地獄

まぁ言いたいことの大半は上の 2 点と関連するのですが、生産性にかかわるであろう部分で分かりやすい実例をあげます。
それは、JSX ではループ地獄から逃れることができない、という点です。


例えば、JSX で number 配列のすべての要素に 10 足す関数を考えてみます。

static function add10(xs: number[]): number[] {
  var ret = new Array.<number>(xs.length);
  for (var i = 0; i < xs.length; i++) {
    ret[i] = xs[i] + 10;
  }
  return ret;
}

これとは別に、string 配列のすべての要素の末尾に "." を追加する関数を考えます。

static function addDot(xs: string[]): string[] {
  var ret = new Array.<string>(xs.length);
  for (var i = 0; i < xs.length; i++) {
    ret[i] = xs[i] + ".";
  }
  return ret;
}

この 2 つ、for の中でやってること以外は同じですよね。
こういう場合には map を使うのが常套手段です。
使ってみましょう。

static function add10(xs: number[]): number[] {
  return xs.map(function(x: MayBeUndefined.<number>): MayBeUndefined.<number> {
    return x + 10;
  });
}

static function addDot(xs: string[]): string[] {
  return xs.map(function(x: MayBeUndefined.<string>): MayBeUndefined.<number> {
    return x + ".";
  });
}

え、本来書きたい部分じゃない所を書く量、そんなに減ってなくね・・・?
これはこう書きたいよなぁ・・・

// このくらい書けるならもはや関数化する意味もなし
static function add10(xs: number[]): number[] {
  return xs.map(function(x) { x + 10; });
}

static function addDot(xs: string[]): string[] {
  return xs.map(function(x) { x + "."; });
}

というわけで、JSX が簡単というのは言い過ぎだろうと思うんですがどんなもんでしょうか?
・・・と、ここまではライトな話で、以降はそうじゃない話です。

バリアントがすべてを台無しにしている

えっと、これ、静的型付きであることを売りにしているんですよね?
それなのになぜ、VB 的なバリアントが入っているんだ、と。

と開発者の一人が untyped について述べてますが、それならこのバリアントはなんなの?と言いたいわけですよ。
untyped がダメなのであれば、バリアントはもっと駄目だと思うのです。

バリアントがない

あ、こっちのバリアントは VB 的なバリアントじゃなくて、直和型とかそんなあれのことです。
例えば haXe には enum という名前でこれが入っていて、enum に対する分岐に switch が使える等、これまで手続型のプログラミング言語を主に使ってきた人でも入りやすい文法になっています。
例えば haXe で

// JSXじゃないよ!
enum Card {
  num(n: Int);
  jack;
  queen;
  king;
  joker;
}

と書くと、

// JSXじゃないよ!
function toInt(c: Card): Int {
  return switch (c) {
    case num(n): n;
    case jack: 11;
    case queen: 12;
    case king: 13;
    case joker: -1;
  }
}

のように、switch 出来るものが簡単に作れます。
これは単純な Java などのような enum ではないことに注目してください。
num は他の列挙子と異なり、値を持っているのです。
これを、case の部分で num(n) とすることで、num が持っている値を取り出して使うことができます。
こういう、switch で使えるユーザ定義型を簡単に、しかも強力に*2サポートしているのは haXe の強みです。


ですが JSX にはバリアントはありません。
ではどうするか。はい。クラス階層を作りましょう、となるわけですね。

// JSXだよ!
abstract class Card {
  abstract function toNum(): number;
}

class Num extends Card {
  var num: number;
  function constructor(n: number) {
    this.num = n;
  }
  override function toNum(): number { return this.num; }
}

class Jack extends Card {
  static var _instance = new Jack();
  static function getInstance(): Jack { return Jack._instance; }
  override function toNum(): number { return 11; }
}

class Queen extends Card {
  static var _instance = new Queen();
  static function getInstance(): Queen { return Queen._instance; }
  override function toNum(): number { return 12; }
}

class King extends Card {
  static var _instance = new King();
  static function getInstance(): King { return King._instance; }
  override function toNum(): number { return 13; }
}

class Joker extends Card {
  static var _instance = new Joker();
  static function getInstance(): Joker { return Joker._instance; }
  override function toNum(): number { return -1; }
}

インスタンスメンバのアクセスに this が必須だったり、クラスメンバのアクセスにクラス名が必須なのもだるいと思いましたが、これはそんなレベルじゃないです。
まぁこの冗長さは自動生成で補うとしましょうか。
ですが、JSX には private とか readonly とかないので*3、せっかく静的型付き言語であるにもかかわらず、null の恐怖からは解放されません。
うーん、readonly あると最適化の余地も増えると思うんですけど、なんでこれないのかよく分かりません。

オーバーロード

オーバーロードできるよ! 静的型付きだもん!

JavaScriptみたいな最近噂の新言語、JSXのお話を聞いてきたよ!

とあるわけですが、静的型付き言語でオーバーロードをわざと許していない言語もあり、それには理由があるのです。
オーバーロードができるのは利点ではなく、単なるトレードオフです*4


さて、JSX では + 演算子を関数として見たとき、どのような型を持っているのでしょうか?
少なくとも、以下の演算は出来るようです。

  • int + int
  • number + number
  • int + number
  • number + int
  • string + string

つまり演算子がオーバーロードされているわけです。
ですが、これだと困ったことが出てきます。型推論に制限ができてしまうのです。
例えば、疑似言語Xでは + 演算子は int と int の足し算にしか使えないとしましょう。
その場合、以下の関数 hoge の型を推論することができるのです。

// 静的型付きの疑似言語X
function hoge(a) {
  return a + 10;
}

この関数は、int を 1 つ引数に取って、int を返す関数です(型を全く書いていないのに型が付く)。
演算子がオーバーロードされている JSX では、この関数の型は推論できません。


このように、オーバーロードを導入するのは単にトレードオフ*5でしかありません。
分かって導入したのであればいいのですが、「これ便利だから入れとこうぜ!」的なノリなのだとしたらその積み重ねは今後、機能同士の衝突となり、矛盾のない形での機能追加が困難になっていくでしょう。
Twitter を見てる限り、型推論を後で強化しよう、という思惑があるように感じたのですが、Scala を参考にすればある程度何とかなる・・・のかな?ちょっと分かりません。

色々書きましたが・・・

期待の言語ではあります。日本発ですし。
まだ生まれたばかりの言語ということもあるので、今後どうなっていくのか非常に楽しみです。
自分としては、何か一つでも突き抜けた言語になって行ってくれると嬉しいかな、と思っています。

*1:と、思っていたら haXe の方が速いらしい?安全性では本物のバリアントとジェネリックを持つ haXe 圧勝でしょう

*2:再帰的なデータ型の定義はもちろん、ネストした先にあるデータの取得も可能

*3:native class のみ __readonly__ が使える

*4:まぁ動的型付きの言語だと、一つのメソッドの中で変数の型による分岐とかが入ってアレなことになりますが、とりあえず動的型付きの言語は置いておきます

*5:オーバーロードの代償として型推論能力を失う