ポクポク

ポクッとしてツナッ

Perl でテンプレートの変数に型を書く in 2017

この記事ははてなエンジニアAdvent Calendar 2017の22日目の記事です。前日は id:takuya-a さんの『Bing検索の裏側―BitFunnelのアルゴリズム』でした。たいへん興味深いですね。


はてなPerl アプリケーションが多い会社ですが、今年のアドベントカレンダーは全く Perl の話が出てきてない......。でも僕は Perl 大好き少年なので、このまえ作った CPAN モジュール Text::Xslate::Bridge::TypeDeclaration の話をします。

一般的な Web アプリケーションではテンプレートエンジンに変数を渡して html を組み立てますよね。 はてなでは Text::XslateTTerse Syntax を利用しているプロジェクトが多いです。 例えばユーザ情報を表示するサイドバーの部分テンプレートはこんな雰囲気になるでしょう。

<!-- _sidebar.html -->

<div id="sidebar">
  <div id="profile">
    <img src="[% user_account.icon_url %]" />
    <a href="[% user_account.profile_page_path %]">[% user_account.name %]</a>
    <p>[% user_account.profile_description %]</p>
  </div>

  [% IF show_recent_entries %]
    <ul id="recent-entries">
      [% FOR entry IN entries %]
        <li><a href="[% entry.permalink_path %]">[% entry.title %]</a></li>
      [% END %]
    </ul>
  [% END %]
</div>

このぐらいのテンプレートなら簡単に把握できるけど、実際はこの30倍ぐらい複雑だぞ!!

  • どういう変数があるのかぱっと見よくわからない
  • それぞれの変数はどのようなオブジェクトなのかよくわからない
  • 分割されたテンプレートは再利用可能で良いけどエンドポイントによって変数の渡し忘れが起きがち
    • 一見ちゃんと動いているように見えたりする

などなど気になりますね。

テンプレート中の変数のコメントの歴史

過去のプロジェクトのテンプレートを見てみると、テンプレート中の変数に立ち向かう人類の努力の跡をみることができます。

暗黒時代

<div id="sidebar">
  ...
</div>
  • ただテンプレートがあるだけ
  • 表示のためにどういう変数が必要かは毎回頑張って読み解く、体力が必要
  • よく壊れるし壊れても気づきにくい

冒頭にコメントを書く時代

[% # user_account:  閲覧対象のユーザオブジェクト
   # show_recent_entries: 最近のエントリを表示するオプション
   # recent_entries: 最近のエントリの配列
%]

<div id="sidebar">
  ...
</div>
  • どういう変数を期待しているかは分かる
  • どういうオブジェクトかは変数名やコメントから読み解く
  • 修正時に同時にコメントを修正するのを見落としがち、レビューで気をつける

コメントに型を書く時代

Smart::Args が流行ってコメントに型を書くようになった頃です。

[% # user_account: 閲覧対象のユーザオブジェクト(Model::UserAccount)
   # show_recent_entries: 最近のエントリを表示するオプション(Bool)
   # recent_entries: 最近のエントリの配列(Maybe[ArraRef[Model::Entry]])
%]

<div id="sidebar">
  ...
</div>
  • 型の表記方法に合意が取れた状態
  • 変数のオブジェクトが何か分かるのでどうアクセスすれば良いか分かる
  • レビューで気をつける世界観はそのまま

型をコメントしつつ動くコードにする時代

型を書く期の少し後はこういうのがありました。

[% SET user_acocunt        = visiting_user_account    # 閲覧対象のユーザオブジェクト(Model::UserAccount) %]
[% SET show_recent_entries = show_recent_entries || 0 # 最近のエントリを表示するオプション(Bool) %]
[% SET recent_entries      = recenet_entries || []    # 最近のエントリの配列(Maybe[ArraRef[Model::Entry]] %]

<div id="sidebar">
  ...
</div>
  • 単なるコメントではなく動くコードではある
  • 変数のデフォルト値を設定したりもやる
  • そこまで何かが良くなっている感はない

などなどいろんな工夫で変数に立ち向かってきたものの、これだ! という方法はありませんでした。

こうしたかった

  • テンプレートに型注釈を書きたい
  • コメントではなく動くコードになってほしい
  • おかしければエラーを出したいし、テストでもチェックしたい

という要求を叶えるために、Text::Xslate::Bridge::TypeDeclaration というモジュールを作りました。

こういう感じにつかえるぞ

[% declare(
    user_account        => 'Model::UserAccount',
    show_recent_entries => 'Bool',
    recent_entries      => 'Maybe[ArraRef[Model::Entry]]',
) %]

<div id="sidebar">
  ...
</div>

変数と宣言している型が合わなければ以下のような感じでエラーがテンプレートに挿入されます。

f:id:pokutuna:20171222015047p:plain

テストでは型エラーの要素があればテストが落ちるようにしています。本番環境ではエラーメッセージから情報が漏れないよう、レンダリングのパフォーマンスを落とさないように型チェックを無効にもできます。 これで複雑なテンプレートを書き換えることになってもヘトヘトにならずに済むぜ!!!

どういう感じか

Type::Tiny を利用して型チェックを行っています。
型ライブラリを利用していないプロジェクトでも導入できるようにデフォルトでは Types::Standard の型を利用可能にし、未定義の型名は isa として扱います。(Smart::Args のような敷居の低さを目指した)

Type::Tiny は特に Tiny な感じはしないけど、{MouseX, MooseX}::Types::Base の型を lookup できるのでプロジェクトの型ライブラリを制限しにくくて良さそうなので採用しました。グローバルな型レジストリが無い点は戸惑ったけどアプリケーションとテンプレートで共通の Type::Registry を使うというのは状態が隠れていなくて良い気もします。

単機能なプラグインを Text::Xslate::Bridge:: 以下に作るのは迷いましたが、Xslate->current_var にアクセスしたかったり、エラーをテンプレートに注入したかったので Bridge として作りました。 Text::Xslate::HashWithDefault と組み合わせて未定義変数への参照も行えばより堅牢なテンプレートを作ることができるでしょう。

ぜひご利用ください!!!

そして明日は id:wtatsuru さんです。たのしみですね。