Haskell(wai) による Webアプリケーション開発の実際

2016年12月追記: この記事のDB(MySQL)編はこちら

wai(Haskell製のWebアプリケーション規格)に準拠し、warp(wai準拠のHaskell製Webサーバ)上で動作するWebアプリケーションの開発手順をまとめる。基本性能を際立たせるため、便利なフレームワークはあえて使用しない。

1. サンプルアプリケーションの概要

サーバの現在時刻をクライアント側で装飾して表示するWebアプリケーション。動作サンプルは下記を参照。

http://mitsuji.org:9997/

このアプリケーションは、下記のエンドポイントからなる。

  • /posixtime — GETでサーバの現在のUNIX時間(ミリ秒単位)を返す。
  • /main.html — 画面のHTML。クライアント側のエントリポイント。
  • /main.js — main.html で読み込まれる JavaScript。posixtime をGETして装飾してから画面に表示する。

2. 開発環境(stack) の準備

下記から stack をダウンロードしてインストールする。解凍してPATHを通すだけでよい。

http://docs.haskellstack.org/en/stable/README.html

下記のようにバージョンが表示できれば準備完了。

$ stack --version
Version 1.0.2, Git revision fa09a980d8bb3df88b2a9193cd9bf84cc6c419b3 (3084 commits) x86_64

3. サンプルアプリケーションのコードの取得

コードは下記に置いてある。

https://github.com/mitsuji/wai-example

ソースコードを clone して、

$ git clone https://github.com/mitsuji/wai-example.git
Cloning into 'wai-example'...
remote: Counting objects: 12, done.
remote: Compressing objects: 100% (10/10), done.
remote: Total 12 (delta 0), reused 12 (delta 0), pack-reused 0
Unpacking objects: 100% (12/12), done.
Checking connectivity... done.

ディレクトリに cd してから、

$ cd wai-example/

下記のコマンドを叩くとghc(Haskellのコンパイラ)などの環境が自動的に準備される。いろいろとダウンロードされるため、初めての時は少し時間がかかるかもしれない。

$ stack setup

ディレクトリ内のファイルのうち、開発に直接関係があるのは下記の三つだけである。

  • app/Main.hs — Webアプリケーション本体のソースコード。
  • static/main.html — Webアプリケーション動作時にアクセス可能となる main.html そのもの。
  • static/main.js — Webアプリケーション動作時にアクセス可能となる main.js そのもの。

4. REPL(ghci) を使用した動作確認

下記のコマンドでHaskell の REPL である ghci に入ることが出来る。

$ stack ghci
The following GHC options are incompatible with GHCi and have not been passed to it: -threaded
Using main module: 1. Package `wai-example' component exe:wai-example-exe with main-is file: /home/mitsuji/Downloads/hoge/wai-example/app/Main.hs
Configuring GHCi with the following packages: wai-example
GHCi, version 7.10.2: http://www.haskell.org/ghc/  :? for help
[1 of 1] Compiling Main             ( /home/mitsuji/Downloads/hoge/wai-example/app/Main.hs, interpreted )
Ok, modules loaded: Main.
*Main>

REPLを使用すると、ソースコード内の関数を直接評価することが出来る。例えば、Main.hs内のこの関数は

getPOSIXTime' :: IO Int
getPOSIXTime' = do
  pt_seconds <- getPOSIXTime
  return $ truncate $ pt_seconds * 1000

このようにして直接評価してみることが出来る。

*Main> getPOSIXTime'
1455622245689

ソースコードを書き換えたときは、下記のようにしてリロードすれば変更が反映される。

*Main> :l app/Main.hs
[1 of 1] Compiling Main             ( app/Main.hs, interpreted )
Ok, modules loaded: Main.

また、下記のようにしてコマンドラインパラメータ付きでmain関数を評価することもできる。この場合はWebサーバが実際に起動する。

*Main> :main 0.0.0.0 9999

ここで、Main.hs 内の関数を見てみよう。

main は Haskell のプログラムのエントリポイントとなる関数であり、プロセス起動時に最初に評価される。ここでは、コマンドラインパラメータからWebサーバの待ち受けIPアドレスとポート番号を取得して、warp を起動している。HTTPリクエスト発生時に評価される関数には routerApp が指定されている。

main :: IO ()
main = do
  host:port:_ <- getArgs
  Warp.runSettings (
    Warp.setHost (fromString host) $
    Warp.setPort (read port) $
    Warp.defaultSettings
    ) $ routerApp

routerApp ではリクエストされたURLを元に評価する関数を振り分けている。/posixtime がリクエストされれば dateAppが、それ以外がリクエストされれば staticApp が評価される。

routerApp :: Wai.Application
routerApp req respond
  | (["posixtime"] == path) = dateApp   req respond
  | otherwise               = staticApp req respond -- static html/js/css files
  where
    path = Wai.pathInfo req

dateApp は リクエスト毎にその時点でのサーバーの時刻をレスポンスとして返している。Int型で得られるUNIX時間をレスポンスとして返すため、Int => String => Text => ByteString(strict) => ByteString(lazy) の変換が行われている。

dateApp :: Wai.Application
dateApp req respond = do
  pt_milliseconds <- getPOSIXTime'
  let pt_lbs = LBS.fromStrict $ encodeUtf8 $ T.pack $ show $ pt_milliseconds
  respond $ Wai.responseLBS H.status200 [("Content-Type","text/plain")] pt_lbs

staticApp は main.html や main.js を レスポンスとして返すための、通常のWebファイルサーバである。ここでは、staticディレクトリ以下のファイルを処理対象としている。

staticApp :: Wai.Application
staticApp = Static.staticApp $ settings { Static.ssIndices = indices }
  where
    settings = Static.defaultWebAppSettings "static"
    indices = fromJust $ toPieces ["main.html"] -- default content

通常のWebファイルサーバであるため、Webアプリケーション動作中はコンパイルや再起動を行わなくても main.html や main.js を編集すれば次回リクエスト時に変更が反映される。

5. runghc を使用した動作確認

REPLに入らずに動作確認したいときは、下記のようにしてmain関数を評価することもできる。Haskellでは、スクリプト的な使い方も可能となっているのである。

$ stack runghc app/Main.hs 0.0.0.0 9999

6. ghc を使用した実行形式バイナリのビルド

下記のコマンドで実行形式バイナリがビルド(コンパイル)され、

$ stack build

下記のコマンドでインストールされ

$ stack install

~/.local/bin/wai-example-exe が生成される。実行形式バイナリの動作確認は下記のようにして行うことができるだろう。

 $ ~/.local/bin/wai-example-exe 0.0.0.0 9999

wai(Webアプリケーションライブラリ)やwarp(Webサーバ)を含むすべてのライブラリがwai-example-exeに静的にリンクされるため、wai-example-exe と staticディレクトリ を ビルド環境と同一アーキテクチャ、同一OSのマシンにコピーすればそのまま動作する。

この時、カレントディレクトリ配下の static ディレクトリ内のファイルが静的Webコンテンツとして参照されるが、ビルド時に settings を下記のように書き換えると、staticディレクトリ内のファイルを実行形式バイナリにすべて埋め込むことができる。静的Webコンテンツも含め、Webアプリケーションを一つのファイルにまとめることができるので、場合によってはとても便利である。

staticApp :: Wai.Application
staticApp = Static.staticApp $ settings { Static.ssIndices = indices }
  where
    settings = Static.embeddedSettings $(embedDir "static")
    indices = fromJust $ toPieces ["main.html"] -- default content

7. まとめ

  • Haskell の 環境構築は stack を使えば超簡単。
  • 一つのソースコードをまったく書き換えることなく REPL、スクリプト、実行形式バイナリの三態から利用可能。
  • WebアプリケーションとWebサーバが一体のため、実行形式バイナリをフロントエンドエンジニア や Webデザイナー に渡せばそのまま開発環境として使ってもらえる。
  • 実行形式バイナリでデプロイすれば、本番に必要な依存環境を最小化できるため、Vagrant や Docker などの環境構築ツールに頼らなくてよい。
  • 静的Webコンテンツを実行形式バイナリに埋め込めば、より安全確実なバージョン管理とデプロイが可能に。

8. おまけ

筆者の利用実績はないが、IDE とか欲しい人向けの情報。

  • Leksah -- Haskell で作られた Haskell用 IDE。
  • Haskell for Mac -- Mac向け。本格的な開発ができるかは不明だけど楽しそう。
  • haskell-idea-plugin -- IntelliJ用 のプラグイン。