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

Haskellで超簡単にWebアプリケーションを作る(モナドも出てこないよ)

はじめに

Haskellは静的純粋関数型プログラミング言語と言われるジャンルの言語であり、型や状態に厳しくWeb開発に向いていないと思われる方が多いと思います。

しかし

  • 型の力によるクリーンアーキテクチャ
  • 抽象化による高級な記述
  • 圧倒的な型推論による軽量言語のような書き心地
  • 意外と高いシングルスレッド性能
  • パフォーマンスも非常に高く, 書きやすい並行並列処理

上記のような利点があり、実はWeb開発に非常に有用な言語であると思っています。

HaskellでのWeb開発における標準的なインターフェースであるwai(web application interface)、そのインターフェースのアプリケーションを動作させる標準的なサーバーであるwarpという二つのライブラリを用いてボトムアップでWebアプリを作っていき、徐々に応用させていこうと思います。

waiwarpはHaskellのWeb開発であるデファクトスタンダードのようなWAFであるYesodServantの内部で使われています。

Version

stackを利用しており、resolverはlts-15.11となります。

Hello Wai!

8000番のポートにアクセスすると常に "hello wai" というレスポンスを返すサーバーを作ってみましょう。

依存するパッケージ

package.yaml
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:8000hello 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

正しくは第二引数にResponseIO 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で作る記事でも書きたいなと思います。

Why not register and get more from Qiita?
  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
No 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
ユーザーは見つかりませんでした