問題意識
モダンというと曖昧ですが、想定しているのはNode.jsのnpmで、pugやsassやtypescriptなど使い勝手の良いalt言語を自動ビルドしながら開発する感じです。
悲しいことに、Haskellのフロントエンド開発は忌避されがちです。
バックエンド(サーバー側)をHaskellで実装する場合、フロントエンド(クライアント側)はどう開発すれば面倒くさくないのでしょうか?
もし、SPAのようにJSON APIを介してバックエンドとフロントエンドを分離するなら、Haskellはバックエンドだけ、フロントエンドはnpmでビルド可能な自由な言語という選択ができます。
例えばservant+elmはいい感じですね。
この場合は、あまり問題にはなりません。
しかし、SPAは一般に管理ファイルとコード記述量が多くなります。
したがって、小規模でブラウザの動的処理が少ない場合は、SPAにしてしまうと逆に面倒です。
あるいは、SEOに問題があるかもしれません。
こういう場合に作るのは、ページ単位で(html, css, js)を返すような従来型のWebアプリです。
本記事では、主にHaskellで実装する小規模のWebアプリを対象に、フロントエンド開発をモダンにする方法を探ります。
Yesod Shakespeare
HaskellにはYesodというWebフレームワークがあり、Shakespeareという型安全なテンプレートエンジンを提供しています。
実のところ、YesodとShakespeareを使えば、自動ビルドしながらのフロントエンド開発は既に可能です。
しかし、Shakespeareの記法に不満を感じる人も多いのではないでしょうか。
下記の例は、Yesod公式ページからの抜粋です。
Hamlet (htmlのalt言語)
$doctype 5
<html>
<head>
<title>#{pageTitle} - My Site
<link rel=stylesheet href=@{Stylesheet}>
<body>
<h1 .page-title>#{pageTitle}
<p>Here is a list of your friends:
$if null friends
<p>Sorry, I lied, you don't have any friends.
$else
<ul>
$forall Friend name age <- friends
<li>#{name} (#{age} years old)
<footer>^{copyright}
Cassius (cssのalt言語)
section.blog
padding: 1em
border: 1px solid #000
h1
color: #{headingColor}
background-image: url(@{MyBackgroundR})
Julius (jsのalt言語)
$(function(){
$("section.#{sectionClass}").hide();
$("#mybutton").click(function(){document.location = "@{SomeRouteR}";});
^{addBling}
});
好みの問題もあると思います。
しかし、私はこれらの記法に中途半端さを感じています。
- Hamlet: 生htmlのようで微妙に違う。。。
- Cassius: これはそんなに悪くないかも?
- Julius: ほぼ生js。。。
URLの埋め込みが特殊(#の代わりに@)で型安全なのは嬉しいですが、pugやsassやtypescriptなど使い勝手の良いalt言語を知っていると、そちらを使いたくなります。
alt言語が今ほど広まってなかった頃は、これで十分だったのかもしれません。
しかし、今のShakespeareは、Haskellerと非Haskellerのどちらから見ても微妙な言語という位置付けになっていると思われます。
Heterocephalus
Shakespeare以外の解決として、pugやsassやtypescriptなどのalt言語をHaskellでうまいこと扱えるライブラリがありました。
Heterocephalusを用いたフロントエンド開発は、大雑把に下記のようになると思います。
- alt言語なんでも(heterocephalusの変数と制御文を埋め込む)を書く
- npm-scriptsなどでビルド、失敗したら1.に戻る
- (html, css, js)(heterocephalusの変数と制御文はそのまま)を得る
- ghcでビルド、失敗したら1.に戻る
- 実行して値を反映した(html, css, js)を得る
alt言語を選ばない汎用性がとてつもないです。
少しだけ触ってみました。
index.pug (heterocephalusの変数と制御文を埋め込む)
<!DOCTYPE html>
html
head
title test
body
h1 test
ul
| %{ forall a <- as }
li \#{a}
| %{ endforall }
index.html (heterocephalusの変数と制御文はそのまま)
<!DOCTYPE html>
<html>
<head>
<title>test</title>
</head>
<body>
<h1>test</h1>
<ul>%{ forall a <- as }
<li>#{a}</li>%{ endforall }
</ul>
</body>
</html>
ghci(Haskellで実行)
> putStrLn . renderMarkup $ let as = ["x", "y"] in $(compileHtmlFile "index.html")
<!DOCTYPE html>
<html>
<head>
<title>test</title>
</head>
<body>
<h1>test</h1>
<ul> <li>x</li> <li>y</li> </ul>
</body>
</html>
※埋め込み文がalt言語に誤解されないよう書き方には工夫が必要です。
コンパイル時間はほとんどかかりませんでした。
したがって、npmとghcのビルドを連結した自動ビルドも現実的と思われます。
しかし、自動ビルドを走らせるには、alt言語から生成されるファイルの更新をうまいことghcに通知しないといけません。
これが厄介そうです。
あまり離れ業はしたくないのが私の気持ちです。
自動ビルドは諦めて、非Haskellerでも気持ちよくフロントエンド開発ができる環境を整えるのなら、このライブラリを使うと良さそうです。
EDSL
Haskellのフロントエンド開発事情について、Shakesphereは今や微妙な解決、Heterocephalusは非Haskellerに優しい解決でした。
となると、Haskellerに優しい解決もあって良いはずです。
そこで、EDSL(言語内DSL)です。
Haskell Wikiに次のように書いてあります。
Embedded domain specific language - Haskell Wiki
Embedded Domain Specific Language means that you embed a Domain specific language in a language like Haskell. E.g. using the Functional MetaPost library you can write Haskell expressions, which are then translated to MetaPost, MetaPost is run on the generated code and the result of MetaPost can be post-processed in Haskell.
一言でいうと、Haskellの文法で他の言語を記述できる仕組みです。
EDSLの良いところは、Haskellの文法を一つ知っているだけで、他の言語が扱うさまざまな領域を操作できる点です。
また、Haskellの関数や値をそのまま利用できるのも利点です。
Haskellでは、モナドという数学的背景のある枠組みで、EDSLを使うことができます。
ShakesphereもEDSLの一つですが、これは独自言語を記述してそれをパースするので、モナドのEDSLではありません。
Haskellに埋め込めはしますが、そのものは外部言語です。
本記事で扱いたいのは、内部言語(モナド)で記述して、他の言語を生成するようなEDSLです。
そこで、(html, css, js)を生成する良さそうなEDSLライブラリを探しました。
見つけたライブラリをモダンなalt言語と対応付けした表が下記です。
使い勝手の良いalt言語 | HaskellのEDSL |
---|---|
pug | lucid |
sass | clay |
typescript, ... etc | sunroof, ... etc |
それぞれ簡単に紹介します。
Lucid (htmlのEDSL)
概要は以下の記事が分かりやすいです。
コード例
html_ $ do
head_ $ do
title_ [] "Introduction page."
link_ [rel_ "stylesheet", type_ "text/css", href_ "screen.css"]
body_ $ do
div_ [id_ "header"] "Syntax"
p_ [] "This is an example of Lucid syntax."
ul_ [] $ mapM_ (li_ [] . toHtml . show) [1, 2, 3]
pugのように見えるでしょうか。
繰り返しにはmapM_
を使います。
includeは、ただの関数切り出しで実現できます。
Clay (cssのDSL)
まだ日本語の記事がないみたいですが、Githubレポジトリの星は結構ついてます。
コード例
header |> nav ?
do background white
color "#04a"
fontSize (px 24)
padding 20 0 20 0
textTransform uppercase
position absolute
left 0
right 0
bottom (px (-72))
sassのように見えるでしょうか。
要素セレクタと結合子、属性と値などが定義されています。
メディアクエリや自動ベンダープレフィックスにも対応していて高機能です。
mixinは、ただの関数切り出しで実現できます。
Sunroof (jsのDSL)
これは見つけるのにとても苦労しました。
LucidやClayと同じように、内部呼び出しが可能な(コード内で結果を得られる)ものに限定した結果、Sunroofが残りました。
経緯は脚注1にメモしてあります。
コード例
sunroofCompileJSA def "main" $ do
function $ \ n -> do
return (n * (n :: JSNumber))
CoffeeScriptのように見えるかもしれません。
CanvasやJQerryなどのAPIが定義されています。
しかし、htmlタグや属性などは自分で文字列を書く必要があります。
実のところ、使い勝手にはあまり満足してません。
ちょっとした動きを実装するのには使えそうな気がします。
EDSLでフロントエンドのコードを書く
以上のEDSLライブラリを使って、フロントエンドのコードを書いてみます。
Lucidのモナドの中で、ClayとSunroofをコードをインラインで埋め込む形で書きます。
lucidHtml :: MonadIO m => HtmlT m ()
lucidHtml = do
doctype_
html_ $ do
head_ $ do
-- ......
style_ [type_ "text/css"] $ render clayCss
body_ $ do
-- ......
js <- liftIO sunroofJs
script_ [type_ "text/javascript"] js
clayCss :: Css
clayCss = do
body ? do
sym margin zero
sym padding zero
fontSize (px 16)
backgroundColor "#c0c0c0"
-- ......
sunroofJs :: IO String
sunroofJs = sunroofCompileJSA def "main" content
content :: JSA ()
content = do
log (string "Debug") console
alert (string "Hello World!")
各EDSLの記述が、Haskellの文法で関数的に結合されていて見通しが良いですね。
idとclassを直和型で一元管理してみる(好奇心)
せっかくEDSLなので、Haskellの直和型を使ってidとclassの名前を一元管理してみます。
手間のわりに嬉しいことは少ないかもしれませんが、試してみます。
{-# LANGUAGE FlexibleInstances #-}
-- | 各DSLの属性型に変換する関数を定義するための型クラス
class ToAttribute a where
toAttr :: IdClass -> a
-- | 型クラスのインスタンス実装(使うDSLライブラリによって実装を変える)
-- | html(lucid)
instance ToAttribute Attribute where
toAttr (Id v) = id_ . pack . kebab . show $ v
toAttr (Class v) = class_ . pack . kebab. show $ v
-- | css(clay)
instance ToAttribute Selector where
toAttr (Id v) = selectorFromText . mappend "#" . pack . kebab . show $ v
toAttr (Class v) = selectorFromText . mappend "." . pack . kebab . show $ v
-- | js(sunroof)
instance ToAttribute JSString where
toAttr (Id v) = string . mappend "#" . kebab . show $ v
toAttr (Class v) = string . mappend "." . kebab . show $ v
-- | idとclassを表現する型
data IdClass = Id I | Class C
deriving (Show, Eq)
-- | idを表現する型(自分で定義する)
data I
= ActionButton
deriving (Show, Eq)
-- | classを表現する型(自分で定義する)
data C
= Container
| Button
deriving (Show, Eq)
適用前
Lucid
body_ $
div_ [class_ "container"] $
h1_ [] "haskellでフロントエンド開発"
Clay
".container" ? do
sym2 padding 0 (em 1)
h1 ? do
color "#6144b3"
fontSize (px 32)
Sunroof 略
適用後(タイポセーフ)
Lucid
body_ $
div_ [toAttr (Class Container)] $
h1_ [] "haskellでフロントエンド開発"
Clay
toAttr (Class Container) ? do
sym2 padding 0 (em 1)
h1 ? do
color "#6144b3"
fontSize (px 32)
Sunroof 略
できました。
自動コンパイルしてみる
モダンなフロントエンド開発にはタスクランナーが必須です。
これをHaskellでやる方法を探ります。
yesod devel
Yesodの自動ビルドは、Yesod以外でも使うことができます。
やり方は大雑把に下記です。
-
自分のアプリからApplication型を切り出す。
(Haskellの大抵のWebフレームワークにはこの型があります) -
Devel.hsという名前のファイルを作成し、DevelExample.hsのdevelMain関数をよしなにコピペする。
-
yesod-binをダウンロード&ビルド
stack build yesod-bin
-
yesod develサーバーを起動
stack exec -- yesod devel
思ったより簡単に導入できます。
しかしWhy is Yesod so slow - Redditの質問のとおり、ビルドする度に少々時間がかかってしまいます。
回答によると、ghciを使うと速いらしいので試してみます。
ghci
私がYesod知らずなだけで、既にYesodのScaffoldに含まれていました。
例えばstack new PROJECTNAME simple
で入るやつです。
DevelMain.hsが提供されています。
Yesode以外で使うには、DevelMain.hsを自分のプロジェクトにコピペして、yesod develと同じくAppliction型を切り出して、update関数よしなに書き換えます。
そして下記のように実行します。
-
ghciを起動
stack ghci
-
DevelMain.hsを読み込む
> :l app/DevelMain.hs
-
アプリを再起動(一瞬!)
> DevelMain.update
-
ファイルを編集する
-
ファイルの変更読み込み(これもほぼ一瞬!)
> :r
-
3.に戻る
速すぎて感動しました。
yesod develとは何だったのか。。。
しかし、DevelMain.hsのコメントにあるように、ghciは非常に残念な欠点を抱えています。
This option provides significantly faster code reload compared to
@yesod devel@. However, you do not get automatic code reload
(which may be a benefit, depending on your perspective)
読み込みが高速な一方、更新は手動になるようです。
replなので当然といえば当然です。
でもそこは対応して欲しかった。
あと一歩のところで諦めきれず、別でうまいことやっている例はないものかと探しました。
ghcid
そしたらなんと、見つけました!
ソースコードを更新すると、自動的に:r
(再読み込み)を実行するghciのデーモンだそうです。
動かし方は下記です。
-
ghcidをインストール
stack install ghcid
-
ghcidをオプション付きで起動
ghcid --command "stack ghci" --test "DevelMain.update"
※ --test: コンパイルが成功するたびに呼ばれる関数を指定
※ DevelMain.update: 前項のghciで使ったものと同じ
たったこれだけで、高速なghciの読み込みが自動化されました!
ヾ(@⌒▽⌒@)ノワーイ!
ブラウザも自動リロードしてみる
最後にブラウザの自動リロードもつけます。
ここはnpmの力を借ります。
browser-syncをインストール
npm install browser-sync -g
ファイル更新を監視しつつ、アプリサーバーをプロキシ
browser-sync start --files src/ --proxy APPSERVER:PORT
※本来はビルド成功時に更新されるファイルを監視すべきですが、いまは手っ取り早くsrc/
を指定しています。
※アプリサーバーのポートが3000の場合、browser-syncサーバーのポートは1ずれて3001で起動します。
結果、npm-scriptsでできるような自動更新をHaskellで実現することができました。
d(`・ω´・+)ヤッタネ+.☆゚+.☆゚
ソースコード
GitHubレポジトリに置きました。
まとめ
本記事では、Haskellのフロントエンド開発をEDSLでモダンにする試みを紹介しました。
細かい点には触れていないので、実際に使ってみるとやや微妙に感じるところがあるかもしれません。
それでも、HaskellでWebアプリを作るときの知見の一つにはなったのかなと思います。
- Haskellのフロントエンド開発にEDSLを使う選択肢があるよ
- Yesod以外でも自動ビルドはできるよ
- ghcid使うと自動ビルドがすごく速いよ
補足
使い勝手に不満が残ったJSのEDSLですが、先進的な解決(フレームワーク)としてMisoやReflexなどがあるようです。
これらも検討してみた方が良さそうです。
追記 2018/11/9
要素のクラスを変更することだけに特化したJSのEDSLライブラリを作りました。
jmonkey🐒
機能は限られていますが、ちょっとした動きを実装するのに役立つかもしれません。
しかし、少し複雑なフロントエンドの実装をするときは、やはり何かフレームワークに頼った方が良いような気がします。
-
JSのEDSL The JavaScript Problem - HaskellWikiに概要が書いてあります。最初はHasteを使おうと思ったのですが、Can't run a Haskell interpreter in a Haste programの説明によると、外部呼び出し以外に生成コードを得る良い方法はないようです。他にもFay, GHCJS、JSaddleなどを検討しましたが、どれも同じ事情があるようでした。そんな中で、唯一Sunroofが内部呼び出し可能なEDSLだったので、ひとまずSunroofを使うことにしました。 ↩