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

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

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

参照用 記事

Webアプリケーションの入出力と状態遷移

入力値の集合がA、出力値の集合がBである関数fを、f:A→B と書きます。fは純関数です。関数が状態に影響を受けるときは、f:S×A→B となります。Sは状態空間です。単に直積の記号「×」では、状態と入力の区別が付かないので、セミコロンで区切ることにします。f:S;A→B 。セミコロンの左が状態ね。fが副作用を持つとき、つまり状態空間Sに作用するときは、f:S;A→S;B と書きます。S→S は状態遷移を表すことになります。

副作用があるかもしれない関数を、次のように分類すると便利です。1は単元集合(シングルトンセット、ユニットセット)です。

  1. f:A→B 純関数
  2. f:S;A→B バートランド・メイヤーの言葉で「問い合わせ」
  3. f:S;A→S;1 バートランド・メイヤーの言葉で「コマンド」
  4. f:S;A→S;B 一度にいろいろするメソッド

以下では、単元集合1は省略します。

メイヤーは、最後の「一度にいろいろするメソッド」を使うとロクなことはないからヤメロと言っています。ただし、現実には「一度にいろいろするメソッド」も登場しちゃいますね。

以上の準備のもとで、Webアプリケーションがどんな入出力と副作用(状態遷移)を持つかを定式化してみます。ここで扱うWebアプリケーションは古典的なもので、ブラウザはリクエスト発行とレスポンスの受理しかしないものとします。Ajaxのようにブラウザ側の自発的動作は考えません。

内容:

  1. サーバー側の挙動
  2. クライアント側の挙動
  3. アプリケーション状態
  4. アプリケーション状態の更新
  5. サーバーとクライアントをまとめた挙動
  6. 代入文の連鎖としてのWebアプリケーション
  7. なにかが抜けている

サーバー側の挙動

ReqをHTTPリクエストの集合とします。1つのリクエストreq(req∈Req)を、URIパスpath、HTTPヘッダーheaders、エンティティボディebodyの組み合わせ (path, headers, ebody) と考えれば、Req = Path×Headers×EBody のような直積分解もできます。が、ここはひとまとまりのReqとして扱います。

HTTPレスポンスのほうはResと書いてもいいのですが、ReqとResで紛らわしいのでDocと書きます。レスポンスはHTML文書のように、なんらかの意味で“文書”(document)と考えていいでしょうから。集合Docの要素docには、Content-Typeのようなメタ情報も含まれます。言葉の本来の意味からは奇妙ですが、404応答などもDocに入るとします。

以上の記法を使うと、サーバー側の処理fは、f:Req→Doc と書けます。この時点では純関数です。しかし、多くのサーバー側処理では、サーバに蓄えられているファイルやデータベースの内容を参照して計算するでしょう。すると、f:State;Req→Doc と書くべきです。読み取り専用のWebサービスならこの形で書けます。fはバートランドメイヤーの意味で「問い合わせ」メソッドになっています。

ブログ記事の新規投稿とかWikiページの編集などをすると、サーバー側リソース(ファイルやデータベースレコード)を変更するので、これは副作用(状態遷移)を伴なう関数で定式化します。次です*1

  • f:State;Req→State;Doc

クライアント側の挙動

クライアントとしてはブラウザを考えます。ブラウザの状態とは、今表示している文書だとします。つまり、状態空間はDocです。状態遷移は Doc→Doc となります。これは「今表示している文書が別の文書に変わる」ことですから、いわゆるページ遷移です。ページ遷移が何をキッカケに起こるかと言うと、HTTPレスポンスを受理するからです。このことは、Doc;Doc→Doc と書けます。

ちと分かりにくいですね。Doccur;Docresp→Docnext と書けば多少マシでしょう。Doccurが今表示している文書=ブラウザの状態、Docrespがレスポンスとして外部からやってきた文書です。オートマトンをご存知なら、Doccur=Docnextが状態空間(状態集合)、Docrespをアルファベット(アクション記号の集合)と思えば分かりやすいと思います。ブラウザを無限状態オートマトンと考えるのです。

ブラウザ側の遷移関数 tran:Doccur;Docresp→Docnext はやたらに簡単で次のように表現できます。

  • (d, d') |→ d'

つまり、直前まで表示されていた文書dがなんであっても、新しくやって来た文書d'で置き換えられてしまいます。ブラウザのページ遷移って、確かにそんなもんですよね。

アプリケーション状態

書籍『RESTful Webサービス』(asin:4873113539)の著者たちは、ブラウザが保持する(と考えるべき)状態をアプリケーション状態と呼んでいます。あまり適切な言葉じゃないと思うけど、彼らの用法と主張に従うことにします。クッキーだのセッションだのは、アプリケーション状態をシミュレートするための手段・方便なので、概念的には「アプリケーション状態=ブラウザの状態」だけを考えればいいことになります。

先に、ブラウザの状態は「現在表示している文書」と言いましたが、アプリケーション状態は、文書以外の状態を意味します。例えば、あるサイトへのログイン状態は次の形のJSONデータと考えていいでしょう。


{
"loggedIn" : boolean, // trueならログインしている
"userId" : (string|null) // ログインしてないときはnull
}

「上のデータはサーバーが持つものだ」と考えている人は考え方を変えてください。本来、サーバーはクライアント状態を持たないのです。クライアント状態(=アプリケーション状態)は当然ながらクライアントが持つのです。現実の多くのケースでサーバーがアプリケーション状態を保持するのは、方便としてです。

さて、文書以外にアプリケーション状態が加わると、ブラウザの状態空間は App×Doc となります。状態遷移は、App×Doc→App×Doc となります。

アプリケーション状態の更新

「HTTPがステートレスだ」ということの意味は、アプリケーション状態の処理が必要なら、アプリケーション状態もクライアントから送られてくるということです。つまり、サーバー側処理への入力にはアプリケーション状態も含ませることになります。サーバー側処理を次の形に手直ししましょう。

  • f:State;App×Req→State;App×Doc

App×Req は、ブラウザから送られてきたアプリケーション状態とリクエストの組です。実際には、アプリケーション状態もリクエストの一部ですが、Appを強調するためにこう書きました。

サーバー側処理を入出力(純関数としての処理)と副作用(サーバー側状態遷移)に分けると:

  • 入出力 App×Req→App×Doc
  • 状態遷移 State→State

例えば、{"loggedIn" : false, "userId" : null} というアプリケーション状態を、{"loggedIn" : true, "userId" : "m-hiyama"} に変えるログイン処理は、サーバー側の純関数処理です。サーバー側の状態遷移ではないのです。「いや、そんなこと言っても…」 -- 僕は今「HTTPがステートレスだ」という原則に則って話しているのであって、現実の実装の中身は問題にしていません!

さて、クライアント側に目を転じます。サーバー側処理の出力である (a', d')∈App×Doc はクライアント=ブラウザにやってきて、ブラウザに受理されます。このときの状態遷移は次のとおり。

  • (a, d) |→ (a', d')

アプリケーション状態も文書も、サーバー側出力(a', d')がそのまま受け入れられて新しい状態になります。ブラウザ自身の自主的判断や動作はありません。

サーバーとクライアントをまとめた挙動

サーバー側処理は、入出力も副作用もある関数とみなせます。クライアントは、サーバー側処理の出力を入力とするオートマトン(状態遷移系)と考えます。

  • f:State;App×Req→State;App×Doc
  • tran:App×Doc;App×Doc→App×Doc

クライアント側の状態遷移関数tranは極めて単純で次のとおり:

  • ((a, d), (a', d')) |→ (a', d')

オートマトンへの入力データがそのまま次の状態となってしまいます。これは、「オートマトン=クライアント=ブラウザ」の挙動はサーバー側処理の出力で決定されてしまうことです。クライアントの挙動を追跡することは、サーバー側処理の出力を追跡することと同じです。

代入文の連鎖としてのWebアプリケーション

サーバー側処理を関数 f(a, r) で表しましょう。aはアプリケーション状態、rは(アプリケーション状態以外の)リクエストです。処理結果である関数戻り値は誰が受け取るのでしょう? クライアントであるブラウザですよね。そのブラウザは、fの出力をそのまま受け入れて自分自身の状態を更新します。旧状態を単に上書きするので、ブラウザは破壊的代入を許すプログラム変数と同じです。

よって、HTTPトランザクションの効果は次の代入文で表現できます。

  • (a, d) := f(a, r);

Webアプリケーションの実体であるfと、ブラウザが何度かやりとりをするなら、それは代入文の連鎖となります。


(a, d) := (null, null); // 必要なら初期化
(a, d) := f(a, r1);
(a, d) := f(a, r2);
(a, d) := f(a, r3);

「クライアントの挙動を追跡することは、サーバー側処理の出力を追跡することと同じ」と言ったのは、タプル変数 (a, d) の値の履歴は、代入文の右辺である f(a, r) という形の呼び出しの戻り値を順に見ていけば分かるからです。

もちろん、Webアプリケーションの挙動を関数呼び出しと代入文とみなすのは単純化し過ぎです。しかし、複雑なものを理解するときには、単純化した形から始めるのは悪くないアプローチでしょう。

なにかが抜けている

以上の定式化では、サーバー側の出力がクライアント側の状態遷移を完全に決定しています。クライアントはまったく受動的で存在感が薄くなります。これは、なにかが定式化から抜けているからです。サーバー側処理が受け取るリクエストの発生機構が全然入ってません。もちろん、リクエストはクライアントが発生させます。

ブラウザはユーザーエージェント(人間の代理)と呼ばれるわけで、「ブラウザ自身が」じゃなくて「人間が」リクエスト発生源です。人間と一緒になったブラウザは自発的な行為が可能となります。レスポンス受理に関しては、サーバー側で生成されたデータを鵜呑みにするだけで、ブラウザがサーバーの奴隷のようでしたが、サーバーは発行されたリクエストをひたすら処理するだけと考えれば、サーバーがしもべとも言えます。

どっちが偉いかはともかく、状態遷移機構を見ている限り、サーバーもクライアントもオートマトンに過ぎず、そこから自発性は出てきません。Webが活発にウゴメキ続けるのは、人間達の世界が動いているからでしょう。

*1:ここでは、サーバー側状態(リソース状態)空間Stateを直接持ち出していますが、Stateに作用するモナド(モノイダルスタンピングモナド)を使ってStateは隠して、モナドのクライスリ圏を使った定式化のほうがカッコイイし、状態遷移がカプセル化され、柔軟性も拡張性も増します。