JavaScriptでパラメータ化enumとパターン振り分けswitch(のようなこと)を簡潔に実現する方法

やりたいこと

JavaScript (ECMAScript) 上で、haXeなどでやっているような、

  • パラメータつき列挙データ定義
  • 定義列挙データごとによる振り分け制御

を実現します。

haXeでのパラメータ化enumが具体的にどういうものかは、

などにサンプルを書いたことがあります。

この機能の直接のルーツは、MLやHaskellのような関数型言語で良く見る、代数データ型宣言(data type)とパターンマッチング(case of)です。enumのパラメータ化とswitchの拡張は、この言語機能の主要素の、手続き型言語への穏やかな導入として使われ始めています。

利用コード例

haXeでの例とほぼ同じように、パラメータ化列挙データ型定義を機能化して、それを利用したミニ言語っぽいものがJavaScript(ECMAScript互換言語)で書けます:

// 列挙データ型定義
var Exp = enums ({
  // データ種: パラメータリスト(パラメータ化しない場合は"")
  Literal: "n",
  Plus: "exp1, exp2",
  Mul: "exp1, exp2"
});

// データ構築の例
var expr = Exp.Plus(Exp.Literal(10), Exp.Literal(19));

// パターン分岐を利用した関数
var evalExp = function evalExp(exp) {
  return when (exp, {
    Literal: function (n) {
      return n;
    },
    Plus: function (exp1, exp2) {
      return evalExp(exp1) + evalExp(exp2);
    },
    Mul: function (exp1, exp2) {
      return evalExp(exp1) * evalExp(exp2);
    }
  });
}

document.write(evalExp(expr));

enumsとwhenが、この機能を実現するために用意した小さな関数です。

言語拡張的な機能でも、見た目を比較的シンプルさせることができるのは、JavaScriptの良いところです。

enumsとwhenの定義

この機能のミソであるenums(enumはキーワードなので避けた)とwhen(switch、caseは以下同文)は以下のような小さな関数です。

function when (exp, cases) {
  return exp(cases[exp.name] || function () {return undefined;});
};

function enums (patterns) {
  var obj = {};
  for (var name in patterns) {
    var args = patterns[name];
    var dctor = null;
    var src = "dctor = function " + name + " (" + args + ") { return function " + name + " (f) { return f(" + args + ");};}";
    eval(src);
    obj[name] = dctor;
  }
  return obj;
};

これだけです。

解説

enumsではリフレクション処理としてコード生成と評価を行っているのですが、理解のため、例題のLiteralとPlusを展開し、同様の結果になるJavaScriptコードでの記述に展開します:

var Exp = {};

Exp.Literal = function Literal (n) {
  return function Literal (f) {
    return f(n);
  };
}

Exp.Plus = function Plus (exp1, exp2) {
  return function Plus (f) {
    return f(exp1, exp2);
  };
}

このように列挙データ型は関数を返す関数として実装しています。

l = Exp.Literal(10)というデータ作成は、コンストラクタ呼び出しであり、関数オブジェクトを一つ作成して返しています。データとして作成した関数オブジェクトは、whenの中で呼び出すために使います。

whenを簡略化し、変数展開すると、

function when (exp, cases) {
  var pattern = exp.name;
  var action = cases[pattern];
  return exp(action);
};

exp.nameは内側のfunction Literal(f)の"Literal"になります。これをパターンpatternとして使っています。
casesからpatternを使って取り出したactionは、 {Literal: function (n) {...},...}のfunctionオブジェクトです。

最後にexp(action)とするとLiteral(f) {f(n);}のfにactionを渡すのでaction(n)、つまり、function(n) {...}のnとは、データを作ったときのExp.Literal(10)の10ということになります。

こうしてめでたく、パターンに合ったfunctionが、データ中のパラメータを受け取って呼び出されることになります。

Visitorパターンとの違い

この機能、振る舞いや目的は大体Visitorパターンのそれらととても似ています。名前の違いのほかには、Visitorはクラスと仮想関数解決を活用したものであるのに対し、これは関数オブジェクトの性質とJavaScriptの文法を活用したものという違いになります。そのため、Visitorを使うとよいような状況では、この手法を選ぶこともできるのではないでしょうか。

制限

簡潔化のため、haXeと比べると以下のようなことはしません。しかし、高機能化することである程度は利用コードの記述性を損なわず対処できるでしょう:

  • 入れ子パターン: 機能的にはwhenの入れ子だけど、簡潔な記法を考えられませんでした
  • パラメータ引数のフォーマットチェック: 正規表現などで入れようと思えば入れられるけど...
  • default(any case): whenでanycaseなど特殊パターンを解釈させるなど。
  • データ型の同値性がない: equalsを実装するために、データ型は同様のことをさせる直接関数を返すのではなく、そのメソッドを持ったオブジェクト型のオブジェクトとして定義することになるでしょうか。
  • データの表示が...: toStringを同上。高機能化するとこの辺が複雑になっていきそう
  • データ型の型チェック:
    • たとえばdctorで同じオブジェクトを参照させ、f(args)の前にそれを持っているかどうかチェックさせる実装で可能
  • パラメータ型: 引数や戻り値に実行時型チェックの機能を入れる必要がありそう。
  • GADT: ...