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

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

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

参照用 記事

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

JsonicをJcentricに改名しました。

さてと、Kuwataさんが実装をすすめているので、そろそろ宣言スタイル・スキーマ定義の構文も固めないと。

注意:この記事の仕様は、最新のCatyに追従していません。執筆時のままの状態です。

内容:

  1. はじめに
  2. ここで述べないこと、それと注意事項
  3. 以前からの変更点
  4. オリジナルJSONスキーマからの拡張と制限
  5. モジュール内のコメント構文
  6. 全体構造
  7. 属性パラメータ
  8. スカラー
  9. 列挙型
  10. 基本型構成子
  11. 配列型
  12. オブジェクト型
  13. 型に働く演算子
  14. 頭部
  15. 不安定な部分

はじめに

なぜ型システムに拘るかというと、Catyある程度は型安全なシステムにしたいからです。型システムがないと型安全もヘッタクレもないからねぇ。でも、あまり精緻な型システムにしてしまうと、学習負担が増えるし、使い勝手も悪くなるので、エエカゲンな妥協点を見つけたいのです。

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

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

これらはどちらも、理論上は芳しくないことで、整合性に悪影響を与えます。でもまー、致し方ないので姑息な手段でツギハギをするつもりです。ツギハギの方法がまだ確定してません。矛盾なくツギハギできるか? 100%の自信はないです(^^;。明日あたり考えるけど。

ツギハギの方法が全体の仕様に影響を与えます。とはいえ叩き台がないと何も考えることができないので、スキーマ定義構文を先に決めます。

ここで述べないこと、それと注意事項

今回は、型の意味論については詳しく述べません。スキーマ定義モジュールのパーザーを書けるようにするだけで、スキーマを使った処理(検証など)については踏み込まない、ということです。

型の意味論に起因する構文的制約があります。例えば、「値や名前の重複(複数回出現)がない」といった条件です。このテの制約は文法(生成規則)では書きにくいで、あまり触れていません。文法で書きにくい制約を構文解析時にチェックするとゴチャゴチャしがちなんで、フェーズを分けて(AST*1ができた後で)やればいいと思います。

また、型を定義する構文だけに絞り、列挙型のラベルのように、ユーザーインターフェースへのヒント情報については触れません*2ユーザーインターフェース情報をスキーマに混ぜるのがいいかどうかも分かんないし。オプショナル型のデフォルト値に触れてないのも似た理由です。

UDIT(User Defined Internal Type)も別な機会に述べます。UDITの定義構文とメカニズムは完全には決まってないし、それは他の要因に影響されます。

構文は全部BNFで定義してますが、あまりきれいでも効率的でもありません。BNF以外の文法が適切な部分もあります(演算子による式とか)。説明用なので、ここに書くBNF自体を尊重する必要はありません。

型(type)と型表現(type expression)が混同されていますが、構文の話しかしてないので混乱はないと思います、型=型表現です。意味論の話をするときは、もう少し注意します。

以前からの変更点

今までモンヤリと記述しただけなので、変更というほどのことではないけど書いておきます。

  1. 型定義文(型定義宣言)の先頭に type を付けることにした。
  2. レジストリーという概念は残るが、公開型は特別な名前を持つモジュールにより実現することにした。
  3. set型は廃止して、bag型を導入。理由は、HTMLフォームからのデータがset型ではなくてbag型なので。
  4. コンパイルインターンはたぶんやらない(やって悪いことはないけど)。
  5. 正規表現の記号「?」、「*」、「|」を採用。
  6. タグ付き型をタグ演算子により表現。「名前=>型」ではなくて「@名前 型」とする。
  7. 主にオブジェクト型の継承のために、インターセクション演算子「&」を導入。この演算子は意味論的に問題ありかも(ちと不安)。

オリジナルJSONスキーマからの拡張と制限

概念的には、バッグ型とタグ付き型を追加しています。ただしこれは、あくまで概念と解釈の話で、インスタンスレベルでは何も追加してません。バッグ型のインスタンスJSON配列で表現されます。タグ付き型のインスタンスは、"$tag" というプロパティを持つJSONオブジェクトで表現されます。

使えるスキーマ属性は大幅に減っています。optionalやadditonalPropertiesのように明らかに不要なものもありますが、解釈がハッキリしないものや、処理と実装の都合で面倒なものは削りました。

モジュール内のコメント構文

次のいずれかがコメントです。

  1. '//' から改行まで
  2. '/*' から '*/' まで

「/* */」スタイルのコメントが入れ子にできると便利ですが、あまり前例がないので、入れ子は認めないことにします。「/* */」のなかに「//」が入るのは当然可能です。

コメントは、空白が許される所に任意に入れることができます。いずれ、ドキュメンテーションコメントを導入する予定なので、パーザーはコメントをAST内に保持すべきです。

全体構造


モジュール ::= 頭部 本体
本体 ::= 型定義文*
型定義文 ::= 'type' (名前 | '$') '=' 型表現 ';'

頭部については最後に述べます。名前は制限がきつくて次の構文(これは字句レベル構文)。


名前 ::= [a-zA-Z][a-zA-Z0-9]*

次は予約語です。星印が付いてるのはすぐには使いません。

  1. integer
  2. number
  3. string
  4. boolean
  5. null
  6. any
  7. never *
  8. array
  9. object
  10. tuple
  11. list
  12. bag
  13. enum
  14. multi
  15. type
  16. internal
  17. module *
  18. package *
  19. provides
  20. requires *
  21. import *

単に名前といったときは予約語を除きます。

属性パラメータ

属性パラメータは、組み込み型名(integer, number, string, boolean, null, any)、型構成子(array, object, tuple, list, bag, multi)、型制限子(enum, multi)に付けることができます。「スキーマ属性名 = 値」という形の並びです。


属性パラメータ ::= '(' スキーマ属性指定並び ')'
スキーマ属性指定並び ::= 空
| スキーマ属性指定 (',' スキーマ属性指定)*
スキーマ属性指定 ::= 名前 '=' 値
値 ::= 数 | 文字列 | 真偽値 | 'null' /* JSONスカラーリテラル */

実際に使える組み合わせは次のとおりです。

スキーマ属性名 値の型 integer number string
minimum -
maximum -
pattern 文字列 - -
minLength 非負整数 - -
maxLength 非負整数 - -
format 文字列 - -
maxDecimal 非負整数 - -

型構成子に関しては:

スキーマ属性名 値の型 array bag
minItems 非負整数 -
maxItems 非負整数 -
minMembers 非負整数 -
maxMembers 非負整数 -

boolean, null, any, object, enum に有効なスキーマ属性は今のところありません。tupleとlistはarrayの特殊な形、multiは bag [enum [...]] の略記です。したがって、tuple, listはarrayと同じスキーマ属性を受け付け、multiはbagと同じスキーマ属性を受け付けます。

スキーマのパーザーは、属性パラメータの正当性をチェックする義務はない(構文のみチェックでよい)ので、null(nonNull = true), object(type="string"), any(name="it", value=3) のような無意味な属性パラメータをエラーとしない可能性もあります。[追記]typeは予約語なので、object(type="string") は、「予約語を名前に使っている」エラーでした。[/追記]

スカラー


組み込みスカラー型名 ::= 'integer' | 'number' | 'string' | 'boolean' | 'null'
組み込みスカラー型 ::= 組み込みスカラー型名 属性パラメータ?

UDIT型を定義すると、あたかも組み込みスカラー型が増えたように振る舞います。日付時刻、金額、バイナリー、郵便番号、クレジットカード番号、メールアドレス、商品コードなどはUDIT型で定義することになるでしょう。UDIT型については詳しく述べませんが、次の定義をしておきます。


スカラー型名 ::= 組み込みスカラー型名 | UDIT型名
UDIT型 ::= UDIT型名 属性パラメータ?
スカラー型 ::= 組み込みスカラー型 | UDIT型 | 列挙型

列挙型

列挙型は、組み込みスカラーの有限個の値を取りだしたものです。制限(restriction)付きのスカラー型です。


列挙型 ::= enum 属性パラメータ? '[' 値並び ']'
値並び ::= 空 | 値 (',' 値)*
/* 値は組み込みスカラー型のリテラル
* ただし、同じ種類の値で重複はなし
*/

enum [true, false] や enum [null] などは無意味に見えますが、ラベルを付けて使うことがあります。例: enum [true : "YES", false : "NO"]; enum [null : "Nothing" ]; 。

列挙型の台型(underlying/carrier type)は、number, string, boolean, nullに限り、しかも、異なる台型の値が混じることを許してないことに注意してください。

基本型構成子

素材となる型(ingredient types)から、新しい型が作れます。次の構成があります。


配列型 ::= 'array' 属性パラメータ? '[' 項目型並び ']
オブジェクト型 ::= 'object' 属性パラメータ? '{' プロパティ型並び '}'
バッグ型 ::= 'bag' 属性パラメータ? '[' スカラー型 ']'
タプル型 ::= 'tuple' 属性パラメータ? '[' 固定長項目型並び ']'
リスト型 ::= 'list' 属性パラメータ? '[' 型 ']'
マルチ型 ::= 'multi' 属性パラメータ? '[' 値並び ']' /* 値並びは列挙型を参照 */

バッグ型の素材型はスカラー型(組み込みスカラー型 | UDIT型 | 列挙型)だけです。マルチ型は、単に「列挙型のバッグ型」の別名です。タプル型とリスト型は便宜上の存在ですが、導入理由は「Jsonic型システムのコア」に書いてあります。

配列型


配列型 ::= 'array' 属性パラメータ? '[' 項目型並び ']
項目型並び ::= 固定長項目型並び | 可変長項目型並び
固定長項目型並び ::= 空 | 型 (',' 型)*
可変長項目型並び ::= 型 '*' | 型 (',' 型)* ',' 型 '*'

可変長項目型並びで使われる星印は、型構成子ではなくて、配列型定義のときだけに使われるマーカーに過ぎません。他の場所では、星印は使えないか別な意味を持ちます。

配列の項目型並びは、意味論的な制約を受けますが、構文上は任意の型を並べることが出来ます。意味論的制約は別な機会に述べます(「Jsonic型システムのコア」にある歯抜け禁止が制約です)。

オブジェクト型


オブジェクト型 ::= 'object' 属性パラメータ? '{' プロパティ型並び '}'
プロパティ型並び ::= 固定長プロパティ型並び | 可変長プロパティ型並び
固定長プロパティ並び ::= 空 | 文字列 ':' 型 (',' 文字列 ':' 型)*
可変長プロパティ型並び ::= '*' ':' 型
| 文字列 ':' 型 (',' 文字列 ':' 型)* ',' '*' ':' 型

プロパティ名である文字列はすべて異なります。可変長プロパティ型並びに登場する、'*'に対応する型は、自動的にオプショナル扱いになるので、「?」を忘れても大丈夫です。

型に働く演算子

今まで登場した型表現は、予約語が先頭に来るので、幾分か関数呼び出しに似てます。型を生成する関数名の後に、値パラメータ/型パラメータが引き続く形式と解釈できます。それに対して、次の構成は演算子の形式で与えられます。

  1. ? (後置演算子)-- オプショナル型
  2. @名前 (前置演算子)-- タグ付き型
  3. & (中置演算子)-- インターセクション型
  4. | (中置演算子)-- ユニオン型

演算子の優先順位は、上の箇条書きの順です。

単項または二項演算子から作られる式は、BNFで書くのは向きません(演算子文法が適切)が、無理矢理に演算子を含む型表現の構文をBNFで書いてみます。予約語が先頭に来る形式の型(実際は型の表現)を、とりあえず「型関数呼び出し」と言うことにします。

まず、次の用語を導入します。

  • 内部型名 -- そのモジュール内の型定義文で定義された型の名前(型定義文の左辺)。
  • 外部型名 -- 他のモジュールで定義された型の名前、モジュール名で修飾される。
  • 公開型名 -- レジストリーを通じて公開されている型名。修飾はない。

さらに、これらを総称して、一般型名と呼ぶことにします。組み込み型名とUDIT型名は一般型名ではなく、型関数(パラメータを取り得る)として扱います。


型因子 ::= 型関数呼び出し | 一般型名 | '(' 型 ')'
オプショナル型 ::= (型因子 | オプショナル型) '?'
タグ付き型 ::= '@' 名前 (型因子 | オプショナル型 | タグ付き型)
型項 ::= 型因子 | オプショナル型 | タグ付き型
インターセクション型 ::= 型項 ('&' 型項)*
ユニオン型 ::= インターセクション型 ('|' インターセクション型)*
型 ::= ユニオン型
型表現 ::= 型

タグ付き型を構成する@演算子は、名前と型を引数とする二項演算子とも取れますが、「@名前」で1つの演算子で、たくさんの前置演算子が存在するとみなすほうが自然でしょう。「@名前」を1個のトークンとしてしまったほうがいいかもしれません。

頭部


頭部 ::= 提供宣言*
提供宣言 ::= 'provides' (名前 | '$') (',' (名前 | '$'))* ';'

提供宣言(provides文)は、モジュール内で定義された型名のなかで、外部からのアクセスを許す型名を列挙します。提供宣言に指定されなかった名前はすべてプライベートな名前とみなされます。名前に重複は許されず、特別な名前(構文的には名前じゃない記号) $ はたかだか1回しか書けません。

不安定な部分

  • スキーマ属性formatは廃止して、UDITを使うことにするかもしれません。
  • enumの台型(underlying/carrier type)は、integerとstringだけに制限してもいいかもしれません。
  • 頭部の構文は未定です。provides宣言の構文も変更の可能性があります。
  • &演算子は意味論的な裏付けが必要。単に集合共通部分では済まないので。

*1:Abstract Syntax Tree; 構文解析の成果物として出来上がるデータ構造です。

*2:enumのラベル(列挙定数名)は、ユーザーインターフェースだけじゃなくて、プログラマも使いますね。