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

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

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

参照用 記事

Catyスクリプト:まだ出来てないけどチュートリアル

http://return0.dyndns.org/log/2009/09/06#s_1

最近ちょっと仕事し過ぎで ...

困ったもんだ。でもまー、Kuwataさんの最近のハードワークのおかげで、CatyもWebフレームワークらしくなってきて、簡単なサンプルサイトが動き出しました。とはいえ、まだまだ仕様が固まってなかったり、暫定仕様/実装でお茶を濁していたり、問題があるのにホッカブリしているところがたくさんあります。

サンプルサイトは、当然にCatyスクリプトで実装しています。
http://d.hatena.ne.jp/m-hiyama/20090810/1249861926 より:

「数十文字のスクリプトファイルが数個」で実現できる動的Webサイトでも、相当に広範囲の要求に応えられるだろうと踏んでいます。

と書いたので、短いスクリプトで動的サイトが出来ないと公約が守れないのですが、それにしても、今のスクリプトはほとんどがワンライナー(一行スクリプト)で、作為的と疑われそうです。元来、Catyスクリプトは複雑なことはできないのですが、それにしても、単純なパイプラインよりはずっと表現力があります。Catyスクリプトの基本的使い方を例で示しましょう。

ソフトウェアを公開してない今の段階で解説を書いても、実感がなくてツマンナイでしょうが、後でまとめて書くことも負担なので、書けることは書いておくことにします。実は、内部文書として書こうとしたのですが、非公開で書く気が起きない。しみじみ僕は NND (Non Non-Disclosure) な人みたい。

内容:

  1. コマンドとパイプ
  2. 例として使う2つのコマンド
  3. 複数のコマンド出力を一緒にしてテンプレートで利用する
  4. スクリプトでデータの加工
  5. 少しずつ確認
  6. それから

コマンドとパイプ

Catyのコマンドは1入力・1出力(1-in 1-out)の関数です。コマンドfooの出力を、コマンドbarの入力に渡すには、foo | bar とパイプ記号を使います。3つ以上のコマンドがあったとき、以下の書き方は意味的にまったく等価です。

  1. (foo | bar) | baz
  2. foo | (bar | baz)
  3. foo | bar | baz

非常に重要な注意事項として; コマンド引数は入力ではありません。引数は、コマンドの動作を指示する情報にはなりますが、入力ではないのです。コマンドfooが、「foo 1」、「foo 2」、「foo 2 hello」のような形で呼び出されるとき、これは、foo-1, foo-2, foo-2-hello という3つの別々なコマンドと考えたほうがいいのです。これら複数のコマンドが、なにかしら似た動作をしたり、同じ目的で使われるので、fooという名前でグルーピングしたと考えてください*1

コマンド名と指定された引数(引数なしも含めて)をまとめてコマンド呼び出しと呼びます。コマンド呼び出しごとに、入力と出力の仕様が決まります。入出力の仕様をプロファイルと呼びます。

例えば、gen-ticketというコマンドは、引数なしで、入力がvoid(入力を必要としない)、出力がstringです*2。このプロファイルは次のように書きます。

  • gen-ticket :: void -> string

コマンドのプロファイル記述には、どうしても型パラメータ(型変数)が必要になります。例えば、なにもせずに入力を出力にコピーするコマンドpass*3のプロファイルは次のようになります。

  • pass :: _T -> _T

アンダスコアからはじまる型名は型パラメータとして使います。

例として使う2つのコマンド

todayというコマンド*4は、次のような型のデータを返します。


object {
"year" : integer,
"month" : integer,
"day" : integer
}

型定義宣言(「Jcentric型システムの宣言スタイル・スキーマ構文」参照)とコマンドのプロファイル宣言を一緒に書くと、次のようになります。


type YMD = object {
"year" : integer,
"month" : integer,
"day" : integer
};

command today :: void -> YMD;

今ログインしているユーザーの情報を、user-infoとうコマンドで取れるとしましょう*5


type UserInf = object {
"givenName" : string,
"familyName" : string,
// その他いろいろ
};

command user-info :: void -> UserInfo;

複数のコマンド出力を一緒にしてテンプレートで利用する

コマンドの出力を、HTMLページに埋め込むにはテンプレートを使います。Smarty風構文を使うことにして、次のテンプレートを考えましょう。

<p>{$user.familyName}{$user.givenName}さん、いらっしゃい。</p>

<p>今日は{$today.year}年{$today.month}月{$today.day}日です。</p>

このテンプレートの中では、$user.familyName, $user.givenName, $today.year, $today.month, $today.day というテンプレート変数を使っています。テンプレート変数の源泉となるデータ=コンテキストとして、次のようなデータを期待していることになります。


type TemplateContext = object {
"user" : object {
"familyName" : string,
"givenName" : string
}
"today" : object {
"year" : integer,
"month" : integer,
"day" : integer
}
};

このようなデータを作るには、user-infoコマンドとtodayコマンドを一緒に実行すればいいだけです。複数コマンドを同時に実行して*6、結果を単一JSONオブジェクトに組み上げるには次のようにします。


{
"user" : user-info,
"today" : today
}

みたまんまです。user-infoからは、familyName, givenName以外の情報も来るでしょうが、それはテンプレートでは使いません(単に捨てられます)。複数の情報供給源からのデータを、ページ内で混ぜて使っているのでローカル・マッシュアップと言ってもいいかも知れません。

なお、コンテキストのデータ型定義とテンプレートが与えられれば、その整合性は「JSONデータにアクセスするパス式の正しさについて」で述べた方法で検証できます。

スクリプトでデータの加工

次に、少し違ったテンプレートを考えます。

<p>{$userName}さん、いらっしゃい。</p>

<p>今日は{$today.year}年{$today.month}月{$today.day}日です。</p>

今度は、コンテキストとして次のようなデータを期待していることになります。


type TemplateContext = object {
"userName" : string,
"today" : object {
"year" : integer,
"month" : integer,
"day" : integer
}
};

練習として、user-infoの出力をスクリプト内で加工してみます。これは、あくまで練習問題です。実際には、テンプレート側で簡単にできることを、スクリプトで行うことはお奨めしません。

さて、user-infoコマンドの出力を、テンプレートが期待している形に加工するには、次のCatyスクリプトを使います。


{
"userName" : (user-info | [getpv familyName, getpv givenName] | concat),
"today" : today
}

はじめて見るとオマジナイに見えますね。説明しましょう。問題は、次のパイプラインです。

  • (user-info | [getpv familyName, getpv givenName] | concat)

パイプラインは左から右へと流れるので、左から順に何が起きるかを見ていきます。まず、user-infoの出力が次のようであったとしましょう。


{
"givenName" : "トン吉",
"familyName" : "板東",
// その他いろいろ
}

getpv <プロパティ名> は、JSONオブジェクトから特定プロパティの値を取り出すコマンド(get property value)です。よって、パイプライン user-info | getpv familyName は、文字列 "板東" を出力します。配列形式に並べられた複数のコマンドには、同じ入力が与えられて、実行結果である出力が配列データに編成されます。

このことから、[getpv familyName, getpv givenName] の出力が配列データ ["板東", "トン吉"] であることが分かります。concat は文字列リスト内の文字列群を連結(連接)するコマンドです。

少しずつ確認

Catyのいいところは、対話的シェル(コマンドプロンプト)があるので、コマンドの動き方を小分けにして、その場で確認できることです。次のようなコマンドラインを実行して、コマンドの出力を目で確認できます。

  1. user-info
  2. user-info | getpv familyName
  3. user-info | getpv givenName
  4. user-info | [getpv familyName, getpv givenName]
  5. ["板東", "トン吉"] | concat

user-infoの出力を直接使う変わりに、test-user.json というファイルにデータを書いておくなら、

  • read test-user.json | [getpv familyName, getpv givenName]

のような方法でもテストできます*7

それから

Catyスクリプトの唯一の制御構造は多方向条件分岐です。任意の条件式が使えるわけではなくて、タグという目印に従って場合分けをするだけです。貧弱すぎる印象がありますが、その印象よりは強力な制御構造となり得ます。次の機会に、この制御構造を紹介します。

*1:コマンドの圏論的セマンティクスは、indexed family of morphisms です。

*2:まだ未実装ですが、ワンタイムチケット(トークン)を生成するコマンドです。

*3:少し前まで、このコマンドはcatという名前にするつもりでした。が、歴史的事情を知らないと意味不明な名前なので、悪しき伝統に従うのは止めることにしました。

*4:現状、なぜかnowという名前になっています。

*5:ユーザー情報は、Caty内の環境変数から取れますが、コマンド仕様はまだ固まってません(実装は簡単でしょう)。

*6:現在の実装は順次実行です。しかし、概念的には並列実行と考えます。

*7:readはファイルから読んだデータを出力に流すコマンドです。これにはcatという別名を付けてもいいかもしれません。