見出しに「Webサービスの設計」という語を含む一連の記事において、「ハイパーリンクこそがWebなんだから、Webでハイパーリンクを使わないなんてあり得ないぞ、使いまくれよな」みたいな話をしてきました。しかしながら、HTML以外のフォーマットでハイパーリンクを「使いまくる」のはなかなか難しいことです。そのへんの事情を話します。
内容:
高機能・柔軟なハイパーリンクの末路
HTMLは、アンカー(a要素)とフォーム(form要素)という形で“決め打ち・作り付け”なハイパーリンク機能を備えています。その機能は限定されており(HTML5でだいぶ増強されるようですが)柔軟性にも欠けます。XMLやJSONをベースフォーマットとするなら、もっと高機能で柔軟なハイパーリンク機能を設計することもできそうです。
と、そう思ってはみるのですが、高機能で柔軟なハイパーリンク仕様はことごとく失敗しています。古くはSGMLベースのHyTime -- 難解膨大な仕様で現実的な実装は登場しませんでした。HyTimeをずっとずっと軽量化してXMLベースとしたXLink -- これなら実装容易と思えたのですが、一筋縄ではいかない問題を抱えていました。HTMLフォームのスマートな拡張と思えたXFormsも普及していません。
この現実をみると、実は一般性なんて必要とされず、HTMLのa要素とform要素と同等な機能性を実現すれば十分なんじゃないのか、と思えます。実際僕はそのように考えて、JSONにもa要素/form要素相当の機能を入れようと思っていました。最近になって、それだけでもうまくいかない状況に遭遇しました。かといって一般化はしたくないし、… というジレンマに陥っています(←今ここ)。
HTMLを真似るという方法
まず僕は、既存の仕様であるJSONハイパースキーマを調べました。そのことは「JSONだってハイパーメディア -- JSONハイパースキーマ仕様をなんとかしたい」に書いてあります。
上記の記事で書いたことは、要するに「ハイパーリンクを表すデータ」という型を、型システムに入れるという話です。reference<T> がT型データへの参照型です。実例を挙げると次のようですね。
/** hCardAdr型の定義 * microformats が定義している住所型 */ type hCardAdr = {"adr" : { "type" : ("work" | "home" | "pref" | "postal" | "dom" | "intl")? "post-office-box" : string?, "street-address" : [string*], "extended-address" : string?, "region" : string?, "locality" : string?, "postal-code" : string?, "country-name" : string?, * : any? } }; /** 人物に関する短い情報 */ type email = string(format="email"); type ShortProfile = { /** 名前の文字列(フォーマット済み) */ "fn" : string(minLength=1), /** メールアドレス */ "email" : email, /** 住所への参照 */ "adr" : reference<hCardAdr>?, }
しかし、参照型を入れるだけではHTMLのハイパーリンク機能を実現できません。そこで、HTMLのa要素とform要素が持つ情報を寄せ集めてトリガー型というのを定義してみました(Webサービスの設計: ハイパーオブジェクトとトリガー)。以前の記事のトリガー型をそのまま引用すると:
type uri = string(format="uri"); type mediaType = string(format="media-type"); type httpMethod = ("GET" | "PUT" | "POST" | "DELETE" | "HEAD"); type Trigger<InputType> = { // 個々のトリガーを識別する属性 "id" : string?, "name" : string?, "class" : string?, // ハイパーリンクの記述 "href" : uri, "rel" : string?, "rev" : string?, "type" : mediaType?, "method" : httpMethod?, // 手続き呼び出し的なデータ項目 "verb" : string?, "input" : InputType, // その他いろいろ * : any? };
このトリガー型を具体化(型パラメータInputTypeを具体的型で置き換える)やサブタイピングすれば、HTMLのa要素/form要素に相当する情報を持つ型を定義できます。
これは、JSONのなかでHTMLをそっくり真似るという方法です。この方法が使える状況では、悪くないアプローチだと思います。
HTMLを素直に真似できないときは
新しくハイパーリンク機能を設計するときは、HTMLを真似ればいいでしょうが、既にあるデータ形式をハイパーリンクとして解釈したいときがあります。例えば、hrefプロパティ(XMLならhref属性)に相当するプロパティの名前がurlだったとします。これだけでHTMLを真似する方法は破綻します。
しょうがないので、urlという名前は「hrefに相当する」という情報をソフトウェアに伝えることになります。リネイム情報とかリマップ情報とか呼ばれ、HyTimeでも初期のXLinkでも使われていました(嫌な予感)。僕も、型定義(スキーマ内)のアノテーションにリマップ情報を入れる方法を考えたことがあります。次の定義のなかで、@[...] がアノテーションです。
@[trigger] type LocationInfo = { @[trigger-prop("rel")] "type" : string, @[trigger-prop("href")] "url" : uri };
アノテーションの意味を日本語で書いてみると:
- @[trigger] -- 以下の型はトリガー型である。
- @[trigger-prop("rel")] -- 以下のプロパティは標準トリガーのrelに相当する。
- @[trigger-prop("href")] -- 以下のプロパティは標準トリガーのhrefに相当する。
rel相当とhref相当のプロパティが同じ階層にあるとは限りません。
@[trigger] type LocationInfo = { @[trigger-prop("rel")] "type" : string, "info" : { "comment" : string?, @[trigger-prop("href")] "url" : uri } };
なんだか汚い。アノテーションで指定するのが良いか? どうも釈然としません。
ハイパーリンク構造の定義は型システムとは切り離す
与えられたデータからトリガーを抜き出して、その情報を解析するのはプログラムです。トリガーがどこにあって、どんな解釈をするべきかをソフトウェアに伝えない限りトリガーの処理(トラバースやリモートアクションの呼び出し)はできません。
逆に言えば、型システムにこだわらずとも、トリガーの場所と解釈方法を効果的に表現する方法があればいいことになります。「カジュアル過ぎるmicroformatsを少しだけ厳密に」の「データ抽出言語とマークアップの正しさ」において、CSSセレクタをベースにしたデータ抽出言語に触れましたが、同じように、トリガー情報抽出言語により、トリガーを記述することができるかもしれません。別な言い方をすると、非標準なトリガーを標準的なトリガーに変換する方法を示すことになります。プロパティのリネイムや階層構造の変更は、変換の一種と考えられます。
こんなやり方がベストだという自信はありません。が、ハイパーオブジェクトとトリガーという概念は、単なる雰囲気や比喩としてだけ語っていてはダメなんです。ソフトウェアにも人間にも明白に分かる記述が必要です。