Catyでは、言語処理系部分をシェルと呼びます。Catyスクリプトは式言語(簡易な関数型言語)ですが、用途としてはコマンド言語/シェル言語なので、Catyスクリプト処理系を「シェル」と呼んでいるのです。
「Catyのインタプリタ=評価関数の表示的意味論」で定義した関数Evalで表されるようなインタプリタ本体と、シェルの組み込みコマンド群をあわせてシェルコアと呼んでいます。シェルコアだけでは、外部世界とのインタラクションができません。シェルコアと外部世界との仲介をする部分をシェルフロントエンドと呼びます。シェルフロントエンドを切り替えることにより、用途やユーザーインターフェースを変化させることができます。
内容:
シェルフロントエンドは何をするのか
Catyスクリプトの式Eがあると、f(x) = Eval(x, E) として関数fが定義できます。この関数fが式Eの表示的意味(denotation)です。関数fは、集合論的始域Aと終域Bを持っていますので、f:A→B と書けます。ただし、A, B⊆Json(AもBもJSONデータの集合)です。集合と関数の圏ではなくて、部分関数の圏で考えれば、f:Json→Json と書いてもかまいません。未定義もエラーも一緒くたに⊥(ボトム)で表現するなら、f:Json → Json+{⊥} という形(Maybeモナドのクライスリ射)と解釈してもいいです*1。
いずれにしても、Catyの内部ではJSONデータ以外は取り扱えません。SとTが、JSONとは限らないデータ領域(値の集合)だとして、S → T+{⊥} という関数をCatyで実現したいときはどうすればいいでしょうか? この目的を達成するのがシェルフロントエンドの役目です。
シェルフロントエンドはこんな感じ
String, Json, Expr はそれぞれ、「文字列(テキスト)、JSONデータ、Catyスクリプトの解析済みの式」を表すデータ型だとします。OpaqueS, OpaqueT は、集合S, Tに対応するデータ型名として使います。
シェルフロントエンド機能の一例を擬似コードで書いてみると次のようになります。
String t = GetScriptText();
Expr e = Parse(t);
OpaqueS s = GetInputData();
Json x = InboundProcess(s);Json y = Eval(x, e); // インタプリタ本体
OpaqueT t = OutboundProcess(y);
PutOutputData(t);
見れば分かると思いますが、念のため1行ずつ説明すると:
- 実行すべきスクリプトテキストを、キーボード、ファイル、Web(HTTP)リクエストなどから取得する。
- テキストを構文解析してAST(Abstract Syntax Tree; 構文解析済み式)を作る。
- パイプラインの最初に置くべき入力データ(JSONとは限らない!)を取得する。
- 入力データを、Catyが処理可能なJSONデータに変換する。
- インタプリタ本体により式を評価する。
- Catyの出力であるJSONデータを外部に出力できる形に変換する。
- 実際に外部に送り出す。
InboundProcessとOutboundProcessが、外部世界のデータとJSONデータの相互変換をします。これは、純粋なデータ変換処理なので、関数ライブラリとして蓄積することができます。Parseはシェル機能の一部です。他の関数(GetScriptText, GetInputData, PutOutputData)は、外部環境ごとに個別に準備する必要があります。
シェルフロントエンドはなんだっていいんだよ
前節で示した擬似コードは、現在のCatyに備わっているコンソールフロントエンドとWebフロントエンドを想定したものです。が、フロントエンドは、Evalに適切なデータを渡せるなら何でもかまいません。
現在のCatyスクリプトは、構文がテキスト表現(text representation)により定義されていますが、絵図表現(pictorial/graphical representation)も可能です。というか、もともと絵図言語だったのを、テキスト言語に直したのです。
例えば、GUIのお絵描きキャンバス上のデータpから、インタプリタが評価可能なデータを作る関数を e = ParsePicture(p) とでもすると、[Eval]ボタンのイベントハンドラを次のようにしてもいいわけです。
Picture p = GetScriptPicture();
Expr e = ParsePicture(p);
Json x = GetInputJson();Json y = Eval(x, e); // インタプリタ本体
DrawOutput(y);
パーズトランスフォーム
先に出した関数Parseは、標準のテキストパーザーのつもりです。それをParsePictureのようなものに取り替えてもいいのです。まー、そこまで劇的にパーザーを変えないまでも、プリプロセスをしたい、シンタックスシュガーをふりかけたいという要求は多いでしょう。個人的には、コマンドのエイリアス機能が欲しかったりします。
要求に応じてパーザーをカスタマイズする一方法として、パーズトランスフォーム(Parse transform)に触れておきます。パーズトランスフォームは、Erlangコンパイラで採用されている技法です。コンパイラの構文解析のタイミングでユーザー定義関数を実行することにより、構文解析に介入できます。
パーズトランスフォーム方式では、パーザーが特定のキーワードやコマンド名に出会うと、対応するユーザー定義処理をコールバックします。これを実現するには、コールバックの約束事を決めた上で、現状のパーザーをイベント駆動方式に書き換える必要があります。面倒ですね。
もっとお手軽な方法として、u: Expr → Expr という関数引数を渡せる高階関数であるパーザーParseTransformを考えてみます。
- ParseTransform : ExprExpr×String → Expr
このParseTransformの定義はすごく簡単で:
- ParseTransform(u, t) = u(Parse(t))
要するに、標準のパーザーを通した後のASTにユーザー定義の変換関数uをかますだけです。次のように使います。
String t = GetScriptText();
Expr e = ParseTransform(u, t);
Json x = GetInputJson();Json y = Eval(x, e);
uはASTのツリー全体を舐めないといけないのであまり効率的ではありませんが、CatyスクリプトのASTは小さいので十分実用になるでしょう。例えば、エイリアス名を見つけて展開するAST変換をuとすれば、エイリアス機能をユーザーが追加できます。
*1:このへんの定式化は、カロウビ展開圏(Karoubi envelope)を使うのが一番いいかな、と思っています。