このブログの更新は Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama

メールでのご連絡は hiyama{at}chimaira{dot}org まで。

はじめてのメールはスパムと判定されることがあります。最初は、信頼されているドメインから差し障りのない文面を送っていただけると、スパムと判定されにくいと思います。

参照用 記事

Jcentric型システム:プラグインによるユーザー定義型

「Jcentric型システムの宣言スタイル・スキーマ構文」

実用上の要求から次のような妥協をしました。

  • インスタンス埋め込みの型情報とユニオン型のラベル(弁別子)を区別しないことにした。
  • プログラムで実装するユーザー定義型をスカラー型に限り認めることにした。

この引用内の「プログラムで実装するユーザー定義型」の話をします。

内容:

拡張ポイントとインターフェース

Jcentric型システムは、単純さを保ち自由度を小さく抑えたいので、ユーザーによるカスタマイズも入れたくはないのです。しかし、JSONには、日付、時刻、金額などの型がありません。スキーマ属性patternがあるので、文字列を正規表現で制限することはできますが、これで何でも解決するわけではありません。ユーザーがプログラムを書くことにより(「スキーマを書くことにより」ではない)新しい型を定義できるメカニズムを入れざるを得ないようです。

プログラムにより新しい型を定義できるメカニズムとは、つまり、型システム(の実装)が拡張ポイントを持つことです。その拡張ポイントの仕様をJavaインターフェースの構文で書いてみると次のようです(実装言語がJavaに限るってことじゃないですからね! 誤解なきよう)。

public interface TypeHandler<Lexical, Value> {
  public Value   convert(Lexical lex) throws TypeHandlerException;
  public boolean isValid(Lexical lex);
  public Lexical normalize(Lexical lex) throws TypeHandlerException;
  public Lexical marshal(Value val);
  public boolean equal(Lexical lex1, Lexical lex2) throws TypeHandlerException;
}

Lexicalは字句空間の型、Valueは値空間の型です。「字句空間(lexical space)」、「値空間(value space)」とは、XML Schema Part2 Datatypesで使われていた用語です。値を表現するための構文的な対象物が字句空間に属し、実際の値は値空間に属します(字句空間と値空間が同じであることもよくあります)。

型ハンドラーの実装を書くときは、型パラメータLexcicalとValueを実際の型に具体化します。例えば次のように(もちろん、メソッドの中身は後でちゃんと書くとします)。

public class CalendarTypeHandler
  implements TypeHandler<String, java.util.Calendar> 
{
  public java.util.Calendar convert(String lex) throws TypeHandlerException {
    return null;
  }
  public boolean isValid(String lex) {
    return true;
  }
  public String normalize(String lex) throws TypeHandlerException {
    return null;
  }
  public String marshal(java.util.Calendar val) {
    return null;
  }
  public boolean equal(String lex1, String lex2) throws TypeHandlerException {
    return true;
  }
}

Javaのインターフェースでは、宣言されたすべてのメソッドを実装クラスで定義する義務がありますが、実は、型ハンドラーのすべてのメソッドを定義する必要はありません。convert, isValid, normalize, marshal, equalが独立ではないのです。ほんとに重要なのはconvertで、convertがあれば、isValidとequalは次のように書けます。

  public boolean isValid(String lex) {
    try {
      java.util.Calendar cal = convert(lex);
      return true;
    } catch (TypeHandlerException ex) {
      return false;
    }
  }

  public boolean equal(String lex1, String lex2) throws TypeHandlerException {
    java.util.Calendar cal1 = convert(lex1);
    java.util.Calendar cal2 = convert(lex2);
    return cal1.equals(cal2);
  }

それに加えてmarshalがあれば、normalizeは、convetrの後にmarshalすることです。

いま詳細は述べませんが、型ハンドラーのメソッド群にはちゃんと定義された厳密な依存関係があるので、人手で全部を書かなくても、一部が与えられれば他は生成できます。おそらく、convertとmarshalを要求する仕様にすると思います(未定だが*1)。

登録キーとオプション

型ハンドラーの実装クラスは型プラグインのレジストリーに登録されます。レジストリーの構造はどうでもいいのですが、登録キー(文字列の名前)と型ハンドラー実装クラスの対応関係を保持します。

フレームワーク(型システムの実装)は、必要なときにレジストリーを引いて、型ハンドラー実装クラスをインスタンス化して使います。生成したインスタンスを毎回捨ててしまうのか、何度も使い回すのかは実装の都合次第です。

ひとつの実装クラスを複数のキーで登録することができます。例えば、先ほどのCalendarTypeHandlerを、dateとdatetimeという2つのキーで登録できます。どのキーによって呼ばれたかによって動作を変える必要があるかもしれません。また、型ハンドラーメソッドの実行時にオプションを渡せれと便利です。オプションによりスキーマ属性も実現できます。

この事情から、型ハンドラーに登録キーとオプションを渡すメカニズムが必要になります。型ハンドラー・インスタンスの生成時のコンストラクター、あるいはセッターによりキーとオプションを注入(inject)してもよいでしょう。メソッドの引数を増やしてもかまいません。

  public java.util.Calendar convert(String key, Options opts, String lex)
      throws TypeHandlerException;

どんな方法がいいかは実装者が決めればいいことです。

UDITの宣言

プラグインプログラムで実装した型をUDIT(User Defined Internal Type)と呼ぶことにします。UDITは、組み込み型(numberやstring)と同じように振る舞うようにします。それは次の要求を満たすことです。

  1. UDITは修飾なしの名前を持つ。
  2. 汎用バリデータなどの処理系は、UDITの名前を前もって知ることができる。
  3. UDIT型名の直後にオプションとしてスキーマ属性指定を書ける。

モジュール名で修飾されない裸の名前を持つ型を公開型(public type)と呼び、publicという名前を持つ特殊なモジュール(型レジストリー)で定義することにします。UDITはpublicモジュール=型レジストリーだけで定義できるとします。スキーマ関連処理系は、最初にpublicモジュールを読むだけで、すべてのUDIT情報を取得できます。

UDITの宣言に必要な情報項目は:

  1. 公開する型の名前
  2. プラグインの登録キー(どの型ハンドラーを使うか)
  3. 受け付けるスキーマ属性(オプション)の仕様
  4. 型ハンドラーにデータとして渡せる型(字句空間の型)

例えば次のような宣言が考えられます。

type dateTime = internal datetime {
                 "timeZone" : string,
                 "locale" : string
                } [string];

この意味は:

  1. 公開する型の名前は dateTime
  2. プラグインの登録キーは datetime
  3. 受け付けるスキーマ属性は、string型のtimeZone属性と、string型のlocale属性
  4. 型ハンドラーにはstring型データを渡す

dateTimeというUDITが登録・宣言されれば次のように使うことができます。

type ProjectDesc = object {
   "start" : dateTime(timeZone = "JST-9"),
   "finish" : dateTime(timeZone = "JST-9"),
   // ...
};

この例でちょっと気になるのは、本来スキーマ属性は制約(制限、述語)を記述するものです。タイムゾーンやロケールは制約というよりは、処理手続きに渡すフラグです。スキーマ定義内に処理手続きの匂いが付くのは好ましくないので、UDIT宣言のほうにまとめて記述しておくほうがよさそうです。(あんまりいい例じゃなかったね、ゴメン。オプションとスキーマ属性の関係は要検討。)

type dateTime = internal datetime {
                 "timeZone" : string = "JST-9",
                 "locale" : string = "ja_JP"
                } [string];

注意事項とか感慨とか

UDITの定義構文は暫定案です。いくつかの問題を解決するために、まだ変更する必要があります。まったく違った構文になる可能性もあります。

UDITはスカラー型にしか許してません。それでも実は、UDIT導入はパンドラの箱を空けてしまいます。型システムの構造は複雑化し、整合的な定式化は何倍も難しくなります。冒頭で「値空間」という言葉を出しましたが、実は値空間こそ諸悪の根元です。字句空間と値空間が一致している(区別の必要がない)からこそ物事が単純に進むのであり、字句空間と別な値空間が必要になると、事情は極端に悪くなります。

そうはいっても、日付時間、金額、バイナリなどは実用上必須です。Catyの内部では、ファイルネームを文字列と区別して扱う必要もあります。実は、バッグ型導入の時点で既に値空間が入って来ています。そのバッグ型はHTMLフォームを扱うために必要なのです。こう考えると、JSONを概念的に一切拡張しないという判断にも無理があるので、まー、致し方ないでしょう。どれほどに酷いことになるかは次の機会に。

*1:equalをプラグイン側に持たせるか、フレームワーク側で処理するかとかが微妙。