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

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

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

参照用 記事

CatyScript コマンド呼び出しの意味論

記述言語がだいたい準備できたので、実際にCatyScript2.0の一部を記述してみるべ。題材はコマンド呼び出し。これは、正確に記述するのが意外に難しい部分なんですよ。

最後のほうで、コマンドラインのパラメータ構文の細かい問題点にも触れます。

内容:

  1. コマンド呼び出しとは何か
  2. パラメータの評価方法
  3. コマンド名の実体への展開方法
  4. コマンド呼び出しの意味と評価方法を記述する
  5. 規則 command-call
  6. 規則 native-command
  7. 規則 script-command
  8. トランスレーションと型検査
  9. コマンドパラメータ構文の細かい問題
  10. 所感とか

コマンド呼び出しとは何か

コマンド呼び出しってのは、まーその名のとおりですね。当たり前すぎて説明が難しいので、今実際に使われている一例を挙げます。


caty:sample> request --debug --method=GET --verb=view /my/todo.memo date=20110214

このコマンド呼び出しは、コマンド名、オプションパラメータ(名前付きパラメータ)、引数パラメータ(位置パラメータ)の3つの部分に分けることができます。コマンド名は "request" でオプションと引数は次のようになります。

オプションパラメータ(名前付きパラメータ):

オプション名 オプション値
debug true
method "GET"
verb "view"

引数パラメータ(位置パラメータ):

引数位置 引数値
第1引数 "/my/todo.memo"
第2引数 "date=20110214"

パラメータは、次のJSONオブジェクトとして1つにまとめることにします。


{
"debug" : true,
"method" : "GET",
"verb" : "view",
"_ARGV" : ["", "/my/todo.memo", "date=20110214"]
}

ここで、_ARGVの最初(0番目)の項目はシステムが設定するもの*1で、コマンドラインからは来ません。

コマンドには、ネイティブコマンドとスクリプトコマンドの2種類があります。ネィティブコマンドはシステム実装言語(現状ではPython)で実装されたコマンドで、スクリプトコマンドはCatyScriptで書かれたコマンドです。

コマンド呼び出しの評価、つまりコマンド実行を記述するとは、次のメカニズムをハッキリさせることです。

  1. パラメータの評価方法
  2. コマンド名の実体への展開方法
  3. コマンド呼び出し全体の評価方法

パラメータの評価方法

パラメータはコマンドライン上に指定されますが、オプションパラメータはJSONオブジェクト式、引数パラメータはJSON配列式として扱います。つまり次のような形の式が与えられたと考えます。

  • オプション:{ "名前1" : 式1, "名前2" : 式2, ...}
  • 引数: [式1, 式2, ...]

JSONオブジェクト式もJSON配列式もCatyScriptの式なので、その評価方法は定まっています(それを述べてはいませんけど、定まるハズです)。パラメータの評価だけ特別扱いしてもろくなことはないので、一般的な評価方法で評価します。もっとハッキリ言うと、パラメータから作った次の式を評価することになります。


{
"名前1" : オプション式1,
"名前2" : オプション式2,
...
"_ARGV" : [引数式0, 引数式1, 引数式2, ...]
}

引数式0は、システムが補完するとします。この式の評価結果がコマンドのパラメータ束縛です。パラメータ束縛は、関数呼び出しにおけるスタックフレームの引数領域のように扱われ、コマンド呼び出しの終了時に破棄されます。

コマンド名の実体への展開方法

コマンドの名前からコマンドの実体を取り出すメカニズムはアンビエント環境に含まれます。Commandを、コマンド名(の文字列)をキーとしてコマンド実体を引けるマップとしましょう。cをコマンド名として、Command[c] には3つの可能性があります。

  1. 未定義。そのような名前のコマンドが存在しない。
  2. ネイティブコマンドへの参照; 参照は文字列形式とする。
  3. スクリプトコマンドの実体; 「command {スクリプトコード}」という形式とする。

未定義のときはエラーです。cがネイティブコマンド(の名前)の場合は、Native[Command[c]] によって、コマンドの実体である関数*2が取り出せるとします。ここで、Nativeマップもアンビエントの一部で、参照文字列からネイティブ関数を引けるマップです。

コマンド呼び出しの意味と評価方法を記述する

さて、「ホーア論理とシーケント計算を混ぜたような意味記述構文」を使って、コマンド呼び出しの意味と評価方法を記述してみます。次の3つの推論規則で十分そうです。


Σ;x{A} → Σ;α Σ,α;x{c} → Γ;y
----------------------------------------[command-call]
Σ;x{c A} → Γ;y


(Native[Command[c]] = f)
-------------------------------[native-command]
Σ,α;x{c} → Σ;f(τ, α, x)


(Command[c] = command {E}) τ,α;x{E} → Γ;y
-------------------------------------------------[script-command]
Σ,α;x{c} → Σ;y

いろいろと補足説明をしないと分かりにくいでしょう。以下に説明を述べます。

規則 command-call

まず、規則[command-call]から。


Σ;x{A} → Σ;α Σ,α;x{c} → Γ;y
----------------------------------------[command-call]
Σ;x{c A} → Γ;y

実際の評価計算は、推論図を逆に(下段から上段に)見たほうがわかりやすいでしょう。下段の式 {c A} において、cはコマンド名、Aはパラメータ式です。{c A} というコマンド呼び出しを、束縛Σと入力xに対して評価したら、束縛がΓになり、結果の値はyとなった、と主張しているのが下段です。

下段の計算の根拠が上段で、パラメータ評価が上段左、コマンド名の展開と評価が上段右です。次のことが読み取れます。

  • パラメータ式Aは、束縛Σと入力xに対して値αと評価される。このとき、束縛Σを変化させない。別な言い方をすると、パラメータ評価では副作用を禁止しています*3
  • 評価済みパラメータαは束縛リスト(スコープチェーン)の一番内側に押し込まれ、新しくできた束縛(スタックフレームに相当)においてコマンド名が展開・評価される。

上段右の Σ,α;x{c} → Γ;y という評価にはさらに根拠が必要ですが、それは規則[native-command]と規則[script-command]によって与えられます。

規則 native-command


(Native[Command[c]] = f)
-------------------------------[native-command]
Σ,α;x{c} → Σ;f(τ, α, x)

コマンド名cがネイティブコマンドを指しているときの評価規則が規則[native-command]です。規則上段の (Native[Command[c]] = f) はアンビエントに関する条件です。つまり、アンビエントのマップを引いた Command[c] がネイティブコマンドへの参照文字列であり、その参照文字列をNativeで解決した結果がfなのです。fはネイティブ関数で、次の3つの引数を取ります。

  1. 大域的束縛 τ (τは環境変数の集まり)
  2. パラメータ束縛 α
  3. 入力 x

ネイティブコマンドの結果とは、関数fを f(τ, α, x) と呼び出した戻り値です。ネイティブコマンドは(スクリプトコマンドもですが)外側の束縛を変更できないので、束縛Σは変化しません。パラメータ束縛αは消費され消えます。

[追記]
次の規則[script-command]とのバランスを考慮して、それと関数fが束縛Σと無関係なことを表現するには次のように書いたほうがいいですね。


(Native[Command[c]] = f) (f(τ, α, x) = y)
----------------------------------------------[native-command]
Σ,α;x{c} → Σ;y
[/追記]

規則 script-command


(Command[c] = command {E}) τ,α;x{E} → Γ;y
-------------------------------------------------[script-command]
Σ,α;x{c} → Σ;y

コマンド名cがスクリプトコマンドを指しているときの評価規則が規則[script-command]です。規則上段の (Command[c] = command {E}) はアンビエントに関する条件です。アンビエントのCommandマップには、cの実体として command {E} という式が格納されていたわけです。command {E} のボディであるEは、τ,α;x{E} → Γ;y と評価されることも前提(上段右)となっています。

この2つの前提から、Σ,α;x{c} → Σ;y が結論されます。この推論規則の意味するところは、「コマンド名がcでパラメータ束縛がαであるコマンド呼び出しは、束縛Σを変化させず、コマンド・ボディの評価結果を出力値とする」です。コマンドボディの評価には、もとの束縛Σを使わず、結果の束縛Γは捨てます。パラメータ束縛αは消費されて消えます。

細かいことを言うと、εを空な束縛として、τ,α;x{E} → Γ;y の代わりに、τ,α,ε;x{E} → Γ;y のほうがいいか? という話があります。空な束縛は、新しく確保したローカル変数領域になります。ローカル変数領域があれば、スクリプトコードE(のトップレベルスコープ)内でパラメータ束縛をシャドーイングすることができます。僕はそんなこと必要ないと思ってますが。

トランスレーションと型検査

現実のCatyでは、ちょっと厄介な問題が残っています。次のコマンドラインを考えてみましょう。


some-cmd --foo=null 123 true

JSONの構文に従うなら、nullはnull型のインスタンス、123は整数、trueは真偽値の真です。しかし、コマンドのオプション値や引数値はJSON構文を採用してません。多くのコマンドラインシェルと同様に、パラメータは(それがリテラルなら)文字列として扱われます。つまり、次のパラメータデータができます。


{
"foo" : "null",
"_ARGV" : ["", "123", "true"]
}

一方で、パラメータ(オプションパラメータと引数パラメータ)にはスキーマが定義されています。例えば、次のような型表現(type expression)がスキーマとなります。


{
"foo" : string?,
"_ARGV" : [string, integer, string*]
}

与えられたパラメータとスキーマと突き合わせれば、文字列を適切な型のデータに変換することができます。例えばこの例では、"123" → 123 と変換することになります。この「スキーマと突き合わて、文字列を適切な型のデータに変換すること」をトランスレーションと呼びます。Catyでは、Webからの入力(クエリー文字列やポストデータ)に対してもトランスレーションを行います。

トランスレーションには型検査も含まれます。例えば、some-cmd --foo=null というコマンド呼び出しは、必須である第1引数がありません。不適切な呼び出しでありエラーとなります。some-cmd --foo=null %bar というコマンド呼び出しでは、%bar が変数参照なので、これを評価した後で型検査(integerであるかどうか)が走ります*4

コマンドのパラメータと標準入力は、コマンドに情報を渡す異なる2つのチャンネル*5ですが、どちらのチャンネルに対しても型検査が実行されます。

コマンドパラメータ構文の細かい問題

伝統的に使われているコマンドライン構文は、キーボードから効率的に入力できて僕は気に入っています。しかし、構文の一貫性と整合性に関して細かい問題があります。

オプション名とオプション値の区切り記号にはいくつかの候補があります。

  1. 空白: cmd --foo val
  2. イコール: cmd --foo=val
  3. コロン: cmd --foo:val

また、真偽値オプションにオプション値は不要です。それどころかオプション値を書けないことが多いでしょう。

オプション名/オプション値の区切りを空白とすると、cmd --foo bar の解釈は曖昧になります。パラメータデータを使って示すと、次の2つのデータがあり得ます。


{
"foo" : true,
"_ARGV" : ["", "bar"]
}

{
"foo" : "bar",
"_ARGV" : [""]
}

cmd --foo=hello bar --baz=world というコマンドラインはどう解釈しますか? 引数パラメータの後に再びオプションの出現を認めるかどうかが問題です。次の2つの解釈があります。


{
"foo" : "hello",
"_ARGV" : ["", "bar" "--baz=world"]
}

{
"foo" : "hello",
"baz" : "world",
"_ARGV" : ["", "bar"]
}

ハイフン2つから始まる文字列を引数パラメータとして渡したいときはどうしたらいいでしょう。

これらの細かい問題に関して僕の意見(むしろ好み)は以下のとおりです。

  1. オプション名とオプション値の区切り記号はイコール。イコールの前後の空白を認める。
  2. 真偽値オプションに値を指定してもよい(しなければtrueとみなす)。値の明示的な指定ができないと、--flag=%switch のような書き方ができないで困ります。
  3. 一度引数パラメータが登場したら、その後のパラメータはすべて引数とみなす。理由は、このほうがルールが単純だから。引数リストを再パーズしてサブコマンドのオプションとして解釈することもできます。
  4. オプションを終了させるには、-- (ハイフン2つだけ)を使う。cmd -- --foo なら、--foo は第一引数。
  5. ショートオプションは使わない*6

所感とか

冒頭で、コマンド呼び出しに関して「正確に記述するのが意外に難しい」と言いました。実際、キチンと説明しようとすると、コマンド名とコマンド実体(定義されたボディ)の対応、パラメータの渡し方、変数の束縛環境、スコーピング、評価戦略、副作用、トランスレーションと型検査などが絡んできます。これら諸々のことを自然言語だけで記述するのは困難です。やはり、形式化した記述言語は必要です。

ホーア論理とシーケント計算を混ぜたような意味記述構文」で述べた記述言語はマー悪くない感じです。ですが、使いやすい記述言語にするにはまだ拡張と改善が必要です。記述言語も記述される対象も、より精密にしたいですね。

*1:コマンド呼び出しの呼び出し元に関するヒントとなる文字列です。

*2:システム実装言語が何であれ、ネイティブコマンド実装は数学的な関数のように考えることにします。

*3:構文的な制約から、そもそも副作用を持つパラメータは書けませんが。

*4:評価の後でトランスレーションをするかどうかは微妙です。今は、不要だろうと思っています。

*5:さらに入出力チャンネルを拡張するにはファシリティという手法が使われます。ファシリティは依存性注入によりコマンドに渡します。

*6:1文字のショートオプションが便利なのは認める、というか個人的に欲しいのですが、デメリットを考えるとやめたほうがいいと思います。ショートオプションがないなら、ハイフンは1個でいいのですが、当面はハイフン2個のほうが落ち着きがいいような、、、