はじめに
Haskellは静的純粋関数型プログラミング言語と言われるジャンルの言語であり、型や状態に厳しくWeb開発に向いていないと思われる方が多いと思います。
しかし
- 型の力によるクリーンアーキテクチャ
- 抽象化による高級な記述
- 圧倒的な型推論による軽量言語のような書き心地
- 意外と高いシングルスレッド性能
- パフォーマンスも非常に高く, 書きやすい並行並列処理
上記のような利点があり、実はWeb開発に非常に有用な言語であると思っています。
HaskellでのWeb開発における標準的なインターフェースであるwai
(web application interface)、そのインターフェースのアプリケーションを動作させる標準的なサーバーであるwarp
という二つのライブラリを用いてボトムアップでWebアプリを作っていき、徐々に応用させていこうと思います。
wai
とwarp
はHaskellのWeb開発であるデファクトスタンダードのようなWAFであるYesod
とServant
の内部で使われています。
Version
stackを利用しており、resolverはlts-15.11となります。
Hello Wai!
8000番のポートにアクセスすると常に "hello wai" というレスポンスを返すサーバーを作ってみましょう。
依存するパッケージ
dependencies:
- base >= 4.7 && < 5
- wai
- warp
- http-types
ソースコード
ソースコード全体はこちらですが、チラ見程度でいいでしょう。
importや、型注釈を消せば実質実装は2行という簡単さです。
{-# LANGUAGE OverloadedStrings #-}
module Main where
import qualified Network.Wai.Handler.Warp as Warp
import qualified Network.Wai as Wai
import qualified Network.HTTP.Types as HTypes
main :: IO ()
main = Warp.run 8000 helloApp
helloApp :: Wai.Application
helloApp req send = send $ Wai.responseBuilder HTypes.status200 [] "hello wai"
こちらのソースコードをビルドして実行すると、localhost:8000
でhello wai
と表示されます。
全体像が掴みやすいように、main関数からトップダウンで一つ一つ解説します
解説
run
main関数はこちらになります。
main :: IO ()
main = Warp.run 8000 helloApp
run関数はApplication
型とPort
型を引数にとって、Webサーバー立ち上げてくれる関数です。
Warp.run :: Port -> Application -> IO ()
PortはIntの型シノニムになっています。
type Port = Int
Application
run関数に渡すApplicationとは何かって話になりますが、ただのリクエストを受けてレスポンスを返す関数です。
type Application = Request -> (Response -> IO ResponseReceived) -> IO ResponseReceivedSource
正しくは第二引数にResponse
をIO ResponseRecived
にする関数(sendやrespondと呼ぶ)が渡されるので、その関数をresponseに適用して返す必要がありますが、私たちの興味はResponse
を構築することだけです。
helloApp :: Wai.Application
helloApp req send = send response -- このresponseを構築したい、あとはsendに渡すだけで良い
responseを構築しましょう。
helloApp :: Wai.Application
helloApp req send = send $ Wai.responseBuilder HTypes.status200 [] "hello wai"
responseBuilder
関数はステータスコードとヘッダーのリストとボディを引数にして、responseを作る関数です。
responseBuilder :: Status -> ResponseHeaders -> Builder -> Response
ここでいうBuilder
とはresponse
を作るという意味ではなく、ボディの文字列の型を指しています。
LazyByteString
型の文字列からresponseを作りたい場合はresponseLBS
という関数も提供しています。
Hello Wai!
これらを組み合わせることで、どんなrequestが来ても ステータスコード200で"hello wai"を返すWebサーバーが完成しました。
{-# LANGUAGE OverloadedStrings #-}
module Main where
import qualified Network.Wai.Handler.Warp as Warp
import qualified Network.Wai as Wai
import qualified Network.HTTP.Types as HTypes
main :: IO ()
main = Warp.run 8000 helloApp
helloApp :: Wai.Application
helloApp req send = send $ Wai.responseBuilder HTypes.status200 [] "Hello Wai!"
ルーティング
上記のアプリケーションはrequestの情報を一切利用せずに常に同じレスポンスを返していました。
requestを利用する第一段階として、URLによるルーティング機能を追加してみましょう。
仕様
rootは最初に作ったレスポンス、追加で"foo"と"not found"を作り、いい感じにルーティングします。
URL | Response | Status Code |
---|---|---|
/ | "hello wai" | 200 |
/foo | "bar buz" | 200 |
other | "not found" | 404 |
ソースコード
ソースコード全体はこちらですが、雰囲気を見る程度で大丈夫です。
{-# LANGUAGE OverloadedStrings #-}
module Main where
import qualified Network.Wai.Handler.Warp as Warp
import qualified Network.Wai as Wai
import qualified Network.HTTP.Types as HTypes
main :: IO ()
main = Warp.run 8000 router
router :: Wai.Application
router req =
case Wai.pathInfo req of
[] -> helloApp req
["foo"] -> fooApp req
_ -> notFoundApp req
helloApp :: Wai.Application
helloApp req send
= send $ Wai.responseBuilder HTypes.status200 [] "hello wai"
fooApp :: Wai.Application
fooApp req send
= send $ Wai.responseBuilder HTypes.status200 [] "bar buz"
notFoundApp :: Wai.Application
notFoundApp req send
= send $ Wai.responseBuilder HTypes.status404 [] "not found"
解説
helloApp :: Wai.Application
fooApp :: Wai.Application
notFoundApp :: Wai.Application
この三つを追加しましたが、特に解説は要らないと思います。
router
routerはパスの情報を判定して上述の各アプリケーションを返す関数とすることで、ルーティングを表現します。
router :: Wai.Application
router req send =
case Wai.pathInfo req of
[] -> helloApp req send
["foo"] -> fooApp req send
_ -> notFoundApp req send
req
という引数はApplication
型の通り、Request
型となっています。
type Application = Request -> (Response -> IO ResponseReceived) -> IO ResponseReceivedSource
Request型
Request
型はHTTPのリクエストをうまく抽象化したレコード型になっていて、リクエストの様々な情報に簡単にアクセスできるようになっています。
フィールドの一覧はこちらです。
data Request = Request {
requestMethod :: H.Method
, httpVersion :: H.HttpVersion
, rawPathInfo :: B.ByteString
, rawQueryString :: B.ByteString
, requestHeaders :: H.RequestHeaders
, isSecure :: Bool
, remoteHost :: SockAddr
, pathInfo :: [Text]
, queryString :: H.Query
, requestBody :: IO B.ByteString
, vault :: Vault
, requestBodyLength :: RequestBodyLength
, requestHeaderHost :: Maybe B.ByteString
, requestHeaderRange :: Maybe B.ByteString
, requestHeaderReferer :: Maybe B.ByteString
, requestHeaderUserAgent :: Maybe B.ByteString
}
リクエストの情報はこちらから全て取得することができます。
これらのフィールドにアクセスするための同名の関数がexportされているので、今回はpathの情報を取得するので、pathInfo関数を利用します。
pathInfo :: Request -> [Text]
pathInfoの戻り値の例はこのようになります。
URL | Return |
---|---|
/ | [] |
/foo | ["foo"] |
/foo/bar | ["foo", "bar"] |
この関数の戻り値をパターンマッチで各アプリケーションに振り分けることにより、routerを再現することができます。
最終的にrouterを走らせることにより、routing機能がついたwebサーバーが立ち上がります。
ソースコード再掲
{-# LANGUAGE OverloadedStrings #-}
module Main where
import qualified Network.Wai.Handler.Warp as Warp
import qualified Network.Wai as Wai
import qualified Network.HTTP.Types as HTypes
main :: IO ()
main = Warp.run 8000 router
router :: Wai.Application
router req =
case Wai.pathInfo req of
[] -> helloApp req
["foo"] -> fooApp req
_ -> notFoundApp req
helloApp :: Wai.Application
helloApp req send
= send $ Wai.responseBuilder HTypes.status200 [] "hello wai"
fooApp :: Wai.Application
fooApp req send
= send $ Wai.responseBuilder HTypes.status200 [] "bar buz"
notFoundApp :: Wai.Application
notFoundApp req send
= send $ Wai.responseBuilder HTypes.status404 [] "not found"
まとめ
HaskellでのWeb開発における使いやすいインターフェースのwaiとそれを簡単に動かしてくれるwarpの紹介でした。
HaskellでもWeb開発いけそうやん!って思ってもらえたら幸いです。
次回予告
これだけだとまともなアプリケーションを作れないので、JSONを返すRESTful APIや、ログイン機能やCookie、Session、DBを利用するアプリケーションなどをwaiで作る記事でも書きたいなと思います。