ローカルトリガーとハイパーメディア・アプリケーション
Catyの型システム/コマンドシステムの機能を総動員すると、Webを使わないハイパーメディア・アプリケーションのハイパースキーマが完全に書ける。つうか先週末に書いてみた。その解説をする。ここ2,3日で定義を変えたりしたところもあるが、オリジナル(つっても4日前)のモノをモトに説明。
まず、通常のトリガー=Webトリガーの場合、本質的なデータ項目は:
- "href": uri,
- "verb": string?,
- "method": httpMethod?,
これによって、ターゲットのサービスエントリーポイント(アクションだと思ってよい)を特定できる。
サービスエントリーポイントへの入力データは次のようだった:
- "inputType": typeName?, (旧inputDatatypeからリネーム)
- "paramsType": typeName?,(旧paramsDatatypeからリネーム)
これはいくらなんでも不十分。値に reif:TypeExpr を使えれば万全だが、現状いきなり reif:TypeExpr は使えない。そこで次の型を使う。
- (@typeName string(format="name") | @data any | reif:TypeExpr)
reif:TypeExprはタグ付き型のユニオンで、@typeName, @data というタグは含まれないので、上記の型は合法となる。将来的には reif:TypeExpr に移行する予定として、当面は、@typeName と @data を使う。
- @typeName string(format="name") : 文字列による型の名前。例: @typeName "mymod:MyType"
- @data any : データリテラルによる固定値
reif:TypeExprが使えれば、@typeName は型の参照で、@data はシングルトン型で表現可能となる。
ローカルトリガーのスキーマ
さて、ローカルトリガーの話だが、とりあえずスキーマを出して、その後で説明を追加する。
module u; // モジュール名は仮 /* ローカル呼び出し/フォーワードを実際に行うためのデータ */ type LocalRequest = @* { /** コマンドのコロンドットパス */ "target": string(format="cdpath"), "arg0": univ, // undefinedにもなることに注意 "args": [univ*], "opts": {*: univ?}(propNameFormat="name"), "additionalEnv": {*: univ?}(propNameFormat="name"), "input": univ, // undefinedにもなることに注意 *: any? }; /** リクエストの実行 * ダミー */ command exec-request :: LocalRequest -> univ { [ $.target > target, $.arg0? > arg0, $.args > args, $.opts > opts, $.additionalEnv > additionalEnv, $.input? > input, ]; // %input? | [%additionalEnv, pass] | unclose {call %target --0=%arg0? %--*opts %#args do { %input? | [%additionalEnv, pass] >: "envInput", [%target, %arg0, %opts, %args] >: "params", } }; /* 値の制約 * 当面、reif:TypeExpr は使わない。 */ type ValueConstraint = (@typeName string(format="name") | @data any /*| reif:TypeExpr */); /* ローカル呼び出し/フォーワードのためのトリガー */ type LocalTrigger = @* { /** コマンドのコロンドットパス */ "target": string(format="cdpath"), "arg0": ValueConstraint, "args": ValueConstraint | [ValueConstraint*], "opts": ValueConstraint | {*: ValueConstraint?}(propNameFormat = "name"), "additionalEnv": ValueConstraint | {*: ValueConstraint?}(propNameFormat = "name"), "input": ValueConstraint, *: any? };
リクエスター、リクエストデータ、トリガー
クライアントはソフトウェアロボット(クライアント役の妖精さん)と考えると分かりやすい。実際、マゾ・テストでは、ロボット(妖精さん)を使う。
クライアントのなかに、リクエスターという部分がある(と考えよう)。リクエスターはリクエストデータ(上記スキーマのLocalRequestのインスタンス)を受け取って、実際にリクエストを発行する。コマンド exec-request がリクエスターの実装だと思ってよい。ただし、ダミーで動かないが。
LocalRequest型のインスタンスがあれば、リクエストを発行できる。だが、ハイパーメディア・オブジェクトにリクエストデータが直接入っているわけではない。トリガー(LocalTrigger)がハイパーメディア・オブジェクトに埋め込まれている。トリガーとリクエストデータは違うものだ!(思考実験: HTMLのa要素はナシにしてform要素だけを考える - 檜山正幸のキマイラ飼育記も参照。トリガー=form要素と考えてよい。)
トリガーというのは、「データの形をしたスキーマ」なのだ。別な某プロジェクトで、「XJSONデータの形をしたスキーマのようなもの」を使っているが、アレと同じ(って、知っているのは二名)。例えば次がトリガーの例(4日前の例)。
{
"target": "mymod:MyClz.foo","arg0": @typeName "null",
"args": [@typeName "string", @data 0],
"opts": {"flag": @data true},
"additionalEnv": {},
"input": @typeName "object"
}
今は @typeName と @data を使っているが、プロパティ値に reif:TypeExpr を使うようになれば、ますますトリガーがスキーマであることが明らかになる。「リクエストデータ : トリガー = インスタンス : スキーマ」なのだ。
リクエスターとは別に、クライアント内にリクエストジェネレータなるモノもあるとしよう。リクエストジェネレータは、トリガー(一種のスキーマ)を見て、トリガーの条件を満たすリクエストデータ(インスタンス)を生成する。現状では、リクエストジェネレータに gen:sample を使っている。
- @typeName t のとき、型の名前tを gen:sample の引数としてデータを生成する。
- @data x のとき、xをそのままデータとして使う。
ハイパーメディア・オブジェクトに埋め込まれたトリガーを見て、リクエスト・インスタンスを生成するジェネレータが賢ければ、ソフトウェア・ロボット(妖精さん)としての精度が上がる。ジェネレータがやることは、「人間がHTMLフォームを埋めること」に相当する。ちゃんと埋められたら賢い。
クライアント・エミュレータ=妖精さん
クライアント役のソフトウェア・ロボットは、クライアント・エミュレータと言ってもいいだろう。何をエミュレートしているかと言うと、本物のアクター、つまり人間のエミュレートである。「小人さん」「妖精さん」などと呼んでいるソフトウェア・ロボットは、だいたい人間の代理となるものだ。
クライアント・エミュレータは次のように作る。
/** クライアントのエミュレーション */ command client [string? start-point] :: (null | HyperMedia) -> never { // ここでトリガーを選ぶ // 選択したターゲットにジャンプ %selected-trigger | call-target-and-client-again }; /** トリガーにより指定されたターゲットを呼び、その後、clientに再入する */ command call-target-and-client-again :: Trigger -> never { // トリガーからリクエストデータをジェネレートして、 // ターゲットを呼ぶ。 // %respose に戻り値であるハイパーメディア・オブジェクト %response | forward mod:client };
clientとcall-target-and-client-againをforwardで繋いでいるのは、begin/repeat構文がまだないから。begin/repeatがあれば、clientの内部で無限ループを回せばよい。そもそも、begin/repeatの主たる使い道は「妖精さん」の内部ループだ。
forwardの使い道
begin/repeat構文があれば、forwardは要らないのだろうか? そうではない、会員と非会員(ゲスト)の区別があるサイトで、それぞれのユーザーロールで振る舞う妖精さんを、client-as-member, client-as-guest とする。ログインしてユーザーロールが変わるとき client-as-guest の内部から client-as-member に大域ジャンプしてロールを変身することになる。
ログイン状態を保つには、クッキー送出のエミュレーションが必要になる。これは、additionalEnv で行う。環境変数の値を送ってやると、行った先の(起動された)コマンド内でその環境変数が見えることになる。コマンドは環境変数を見て挙動を変えることができる。
Web起動とローカル起動の対応関係は:
例えば、{"SECURE": true} という additionalEnv を送ると、https通信をエミュレートできる。USER_IDとかLANGUAGEなんて環境変数もよく使うだろう。
マルチクライアント
マルチクライアントの状況をエミュレートするには、複数のエミュレータ(妖精さん)を同時に走らせる必要がある。通常のスレッド/プロセスでは重すぎて扱いにくい。軽量なグリーンスレッドが欲しい。Pythonにもグリーンスレッドの試みはあるようだし、数十個程度なら uWSGI でもグリーンスレッドをサポートしているようだ。
クライアント・エミュレータ1個に対してグリーンスレッドを1個割り当ててbegin/repeatループを回す。たくさんの妖精さん達がワサワサと動きまわる。楽しそうだ。
hconアプリケーション
通常のWebアプリケーションとローカル(インターナル)で動くハイパーメディア・アプリケーションの中間に、hconアプリケーションがある。hconアプリケーションでは、コマンド呼び出しにhconプロトコルを使う。
hconプロトコルは、トランスポートにHTTPレイヤーを使うが直接呼び出しに極めて近い。「ローカル・アプリケーション ⇔ hconアプリケーション」の相互変換は容易だろう。