このブログは、旧・はてなダイアリー「檜山正幸のキマイラ飼育記 メモ編」(http://d.hatena.ne.jp/m-hiyama-memo/)のデータを移行・保存したものであり、今後(2019年1月以降)更新の予定はありません。

今後の更新は、新しいブログ http://m-hiyama-memo.hatenablog.com/ で行います。

ローカルトリガーとハイパーメディア・アプリケーション

Catyの型システム/コマンドシステムの機能を総動員すると、Webを使わないハイパーメディア・アプリケーションのハイパースキーマが完全に書ける。つうか先週末に書いてみた。その解説をする。ここ2,3日で定義を変えたりしたところもあるが、オリジナル(つっても4日前)のモノをモトに説明。

まず、通常のトリガー=Webトリガーの場合、本質的なデータ項目は:

  1. "href": uri,
  2. "verb": string?,
  3. "method": httpMethod?,

これによって、ターゲットのサービスエントリーポイント(アクションだと思ってよい)を特定できる。

サービスエントリーポイントへの入力データは次のようだった:

  1. "inputType": typeName?, (旧inputDatatypeからリネーム)
  2. "paramsType": typeName?,(旧paramsDatatypeからリネーム)

これはいくらなんでも不十分。値に reif:TypeExpr を使えれば万全だが、現状いきなり reif:TypeExpr は使えない。そこで次の型を使う。

  • (@typeName string(format="name") | @data any | reif:TypeExpr)

reif:TypeExprはタグ付き型のユニオンで、@typeName, @data というタグは含まれないので、上記の型は合法となる。将来的には reif:TypeExpr に移行する予定として、当面は、@typeName と @data を使う。

  1. @typeName string(format="name") : 文字列による型の名前。例: @typeName "mymod:MyType"
  2. @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 を使っている。

  1. @typeName t のとき、型の名前tを gen:sample の引数としてデータを生成する。
  2. @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起動とローカル起動の対応関係は:

  1. リクエストのエンティティボディ ⇔ 標準入力
  2. クエリーパラメータ ⇔ オプション
  3. リクエストURLのパス ⇔ arg0
  4. リクエストヘッダ ⇔ 追加環境変数

例えば、{"SECURE": true} という additionalEnv を送ると、https通信をエミュレートできる。USER_IDとかLANGUAGEなんて環境変数もよく使うだろう。

マルチクライアント

マルチクライアントの状況をエミュレートするには、複数のエミュレータ妖精さん)を同時に走らせる必要がある。通常のスレッド/プロセスでは重すぎて扱いにくい。軽量なグリーンスレッドが欲しい。Pythonにもグリーンスレッドの試みはあるようだし、数十個程度なら uWSGI でもグリーンスレッドをサポートしているようだ。

クライアント・エミュレータ1個に対してグリーンスレッドを1個割り当ててbegin/repeatループを回す。たくさんの妖精さん達がワサワサと動きまわる。楽しそうだ。

hconアプリケーション

通常のWebアプリケーションとローカル(インターナル)で動くハイパーメディア・アプリケーションの中間に、hconアプリケーションがある。hconアプリケーションでは、コマンド呼び出しにhconプロトコルを使う。

hconプロトコルは、トランスポートにHTTPレイヤーを使うが直接呼び出しに極めて近い。「ローカル・アプリケーション ⇔ hconアプリケーション」の相互変換は容易だろう。