Help us understand the problem. What is going on with this article?

heterocephalus - Haskellとフロントエンドが仲良くできるテンプレートエンジン

More than 1 year has passed since last update.

問題提起

近年、フロントエンドを Elm1React.js などで実現することが多くなり、Webサーバに求められる役割は、JSON形式のAPIを提供するだけでよい例も増えてきました。
しかし、規模によってはHTMLにサーバサイドで変数を埋め込んでそのまま表示したい案件も多く存在します。

JSON形式で良ければaesonなどの恩恵にあずかれば良いのですが、Haskellで書かれたWebサーバで、バックエンド側の変数を埋め込んだHTMLレスポンスを返すにはどうしたらよいでしょうか?

うーん

Yesodの解決策と課題

HaskellのWebフレームワークとして歴史のあるYesodでは、Shakespeareを推奨しています。
Shakespeareは、Haskellっぽいインデントベースの文法を強制されるというデメリットがありますが、Template Haskellによってコンパイル時にテンプレートに埋め込まれた変数の型や文法をチェックしてくれます。
日頃からHaskellを使っているような、コンパイラに頼らなければバグを埋め込みまくってしまう我々には必須の機能ですね!

  • テンプレートに出てくる変数がHaskell側で定義されていない
  • テンプレートに出てくるfor文の対象が配列やリストでない
  • Haskell側で定義した変数が、テンプレートで使われてない

など、コンパイラがありえそうなランタイムエラーを未然に防いでくれます。

でも、Shakespeareの、あの独自記法に魂を売ることに、本当に同意していますか?
向上心あふれるHaskellerのみなさんの中には、昨今のWebフロントエンドで使われる gulp や webpack などのツールを使いたがる方もいることでしょう。
これらのツールと相性がよい、pug や slim、haml などのテンプレートエンジンを使えず、Shakespeare を強制されるのは、筆を選ばないことで有名な弘法大師でも堪えられないでしょう。
モックアップなどの小さなアプリなら、一時的に魂を売ることに抵抗はないのですが。。。

あるいは開発規模によっては、HTMLやCSSのコーディング作業以外なにも学ばずに何年も生きているのに、片手間でやっている我々よりも質の悪いコードを書ける方に、HTMLの記述部分だけはまかせないといけないこともあるでしょう。
もちろん、彼らにShakespeareを書いてもらうことは不可能です。
なぜなら彼らは脳みそにふさふさのカビが生えてしまって、新しいことを覚えたくても、残念ながらそれができないのですから。

疲れた

型安全でないテンプレートエンジンたち

Haskell界には多くのテンプレートエンジンが存在するので、必ずしも Shakespeare に拘泥する必要はありません。

下記の通りいろいろあるので、好きなものを選ぶことができます。

しかし、これらのどれもが、Haskellコンパイル時にテンプレートファイルの型チェックを行ってくれません。
我々が、これらを使いこなせるような人間型検査機であったなら、いまごろは動的型付言語を使いこなせる幸せな人生を歩めたことでしょう。

救世主 haiji への弾圧

そんな中、@notogawa さんの haiji が、コンパイル時にテンプレートを型検査する機能を提供してくれていました。
もちろん、Shakespeare のように、テンプレートにインデントを強制するものではなく、Jinja2っぽい、HTMLをベースにした記法で、たくさんの便利機能を提供してくれます。(参考記事)

しかし不幸なことに、ghc 7.10以降では、stackで管理されたプロジェクトで haiji を使うと、extraordinaryなコンパイル時間が必要になってしまいました。
もちろん、それを我慢すれば多くの恩恵を得られるのですが、さすがにつらすぎます。メインメモリの容量が少ないマシンだと、フリーズして、そもそもコンパイルできないかもしれません。

残る選択肢

そうなると、Shakespeare が提供する Text.Shakespeare.Text 以外に選択肢はありません。
これは、インデントを強制するShakespeareの各種テンプレートエンジンの中で、唯一特殊な記法を強制しないライブラリです。
しかし、プレインテキストを主な対象としているため、基本的なifforなどの制御機能が存在せず、結局サーバサイドにViewをがっつり構築する必要があります。これでは、フロントエンドとの協働は困難を極めますし、サーバサイドで構築したスニペットをエスケープなしでテンプレートに埋め込まなければならないため、XSS脆弱性の温床になります。

テンプレートファイルの例。
エスケープなしで変数の内容を読み込む必要がある。

(略)
<div class="foo-list">
  ^{rawHtml}
</div>
(略)

サーバサイドの例。
Viewをサーバサイドで構築する必要がある。

template elements = textFile "template/sample.html"
 where
  rawHtml = unlines . (`map` elements) $ \elem -> elem
    "<p class=\"foo-element\">" <> elem <> "</p>"

めんどくせぇ

はだかでばねずみちゃん

heterocephalus

さすがに詰んだので、heterocephalusというライブラリを作りました。
ちょうど今日、日本語も堪能でHaskell力も高い英語ネイティブとして有名なcdepillaboutさんが僕のつたない英語ドキュメントをかっこ良くしてくれました。
(彼はARoWというイケてる会社で働きながら、フリーランスとしてもおもしろい仕事を探しているようです)

heterocephalus は
stack install heterocephalus --resolver nightlyで使えるようになります。

以下、heterocephalus の特徴です。

コンパイル時にテンプレートを型検査

コンパイルの段階で、テンプレートを Parsing します。
テンプレートを外部ファイルに分離した場合も、コンパイル時にそのファイルを読み込み、独自のASTに変換して保持します。
この時、下記の型チェックを行います。

  • テンプレートの文法が正しいか検証
  • テンプレートに埋め込まれた変数の型が正しいか検証
  • スコープに存在する変数が、プログラム内でもテンプレートでも使われていない時にWarning

コンパイル時にASTに変換するので、ランタイムにテンプレートファイルを読み込む必要がなく、以下の副次効果も得られます。

  • ランタイム時のIOレイテンシを抑制できる
  • コンパイルした実行ファイルさえあれば動く

基本的な使い方

{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE QuasiQuotes #-}
import Text.Blaze.Renderer.String (renderMarkup)
import Text.Heterocephalus (compileTextFile)

main :: IO ()
main = do
  -- "templates/sample.txt"で参照する変数
  -- 実行時に値が決定される変数にも、コンパイル時に型の整合チェックがなされる
  as <- readLn :: [String]
  putStr $ renderMarkup $(compileTextFile "templates/sample.txt")

インデントを強制しないテンプレート構文

テンプレートファイルに対して、Shakespeare のようなインデントを強制しません。

テンプレートの例。
インデントや改行はとくに強制しない構文を提供する。

(略)
<div>
  #{ bar }
</div>
<div class="foo-list">
  %{ forall (k,v) <- kvs }
    %{ if isValidKey k }
      #{k}: #{v}
    %{ endif }
  %{ endforall }
</div>

変数埋め込み

以下の形式で変数を埋め込めます。
また、#{ } の内部で、型コンストラクタや関数を使うことも可能です。2

#{ var }
#{ even num }
#{ num + 3 }
#{ take 3 str }
#{ maybe "" id (Just b) }

変数エスケープ

heterocephalus は特にHTML専用のテンプレートエンジンではなく、JavaScriptやCSSにも埋め込める汎用のものですが、HTML用の便利な変数エスケープを提供しています。
また、独自のエスケープルールを指定することもできます。

compileTextは、compileTextFileのQuasiQuoter版。
埋め込み変数をエスケープしない。

>>> renderMarkup (let as = ["<a>", "b"] in [compileText|sample %{ forall a <- as }key: #{a}, %{ endforall }|])
"sample key: <a>, key: b, "

compileHtmlは、compileHtmlFileのQuasiQuoter版。
compileHtmlおよびcompileHtmlFileは、埋め込み変数をHTMLエスケープする。

>>> renderMarkup (let as = ["<a>", "b"] in [compileHtml|sample %{ forall a <- as }key: #{a}, %{ endforall }|])
"sample key: &lt;a&gt;, key: b, "

制御文

制御文は、forallifのみを提供します。

%{ forall x <- xs }
#{x}
%{ endforall }

%{ forall (k,v) <- kvs }
#{k}: #{v}
%{ endforall }

%{ if even num }
#{num} is even number.
%{ else }
#{num} is odd number.
%{ endif }

別のテンプレートエンジンとの共存を想定した設計

「制御文がたった2つしかないなんて使いづらい」と思いましたか?
heterocephalusは、あえて豊富な制御文を提供しません。
提供するのは、フロントエンド側で好きなテンプレートエンジンを使って生成したファイルに対して、さらにバックエンド側の変数を埋め込むための機能だけです。

heterocephalusの利用イメージ

heterocephalus単体で全部まかなえるように複雑な構文を提供するよりも、基本的にユーザが好きなテンプレートエンジンを使えた方が便利だという思想のもとにこのような設計になっています。

さらに詳細

これ以外にも、変数スコープのデフォルト値を指定する機能などもありますが、詳細はgithubレポジトリhackageをご覧ください。

WebフレームワークSpockで heterocephalus を利用しているプロジェクトもgithubで公開されています。
Yesod や Servant で heterocephalus を利用する方法など、きっと誰かがブログにしてくれることでしょう。

まとめ

以上、heterocephalus によって、Haskell プロジェクトが近代的なフロントエンド開発環境とも協働できることを示しました。
heterocephalus は PR や issue も大歓迎です!

スケボーうさぎ


  1. Elm Advent Calendar 2016はまだ空きがありますよ!! 

  2. 一番下のmaybeの例は現実的な例ではないですが... 

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away