Haskell による Web Service 構築入門

Daichi TOMA
Jul 6, 2013

Haskell とは

純粋関数型プログラミング言語です。

純粋なので、一度変数の値を設定すると、変更することは出来ません。

関数型言語では、引数に関数を作用させていくことで計算を行います。

他にも遅延評価や、強い静的型付けなどの特徴があります。

Haskell の特徴

強い静的型付けにより型規則に従ってない式が存在しないことを保証します。

また型推論を持つため、すべての式に明示的に型を書く必要はありません。

しかしながら、実際にはどのような関数なのか表すために明示的に型を書くほうが良いです。

コンパイルが通れば概ね思い通りに動くのもHaskellの特徴です。

Haskell の導入

一番手っ取り早い方法は、Haskell Platformを導入することです。

Haskellのコンパイラで最も広く使われている The Glasgow Haskell Compiler (GHC) や、便利な Haskell のライブラリのセットが付いてきます!

http://www.haskell.org/platform/にアクセスして、利用しているOS向けの指示に従ってください。

GHCiの起動

Terminal を開き、

$ ghci
とタイプすることで、対話モードが起動できます。

対話モードでは、実際に関数を呼び出して、結果を直接見ることができます。

対話モードを終了するには、

ghci> :q
とタイプし、ENTERを押します。

GHCiで遊んでみる

ghci> 2+3
5
ghci> succ 3
4
ghci> 2 > 3
False
ghci> True && False
False
gchi内で関数を定義する際はletが必要
ghci> let doubleMe x = x + x
ghci> doubleMe 4
8
ghci> let doubleUs x y = doubleMe x + doubleMe y
ghci> doubleUs 2 3
10
ghci> doubleUs 1.2 4.5
11.4

なぜ Haskell で Web Serivce を書くのか

Warp

軽量、高速 HTTP サーバです。

Haskell の軽量スレッドを活かして書かれています。

Pong benchmark (req/s) Preliminary Warp Cross-Language Benchmarks

とりあえず、Warp 入れてみる

$ cabal install warp

cabal を使えば簡単に入れられます。
cabal とは Haskell の Package 管理システムです。

インストールされているPackage、場所などは以下のコマンドで確かめられます。

$ ghc-pkg list

HaskellによるWeb Service

今日の例題を見ていきます。
https://gist.github.com/amothic/5938617

この例題は、URLによって出力する結果を変更するWeb Serviceです。 また、/welcome/worldへアクセスした場合、インクリメントされるcounterが表示されます。

HaskellによるWeb Service

{-# LANGUAGE OverloadedStrings #-}
import Network.Wai
import Network.HTTP.Types (status200, status404)
import Network.Wai.Handler.Warp (run)
import Control.Monad.Trans (lift)
import Data.IORef (newIORef, atomicModifyIORef)
import Data.ByteString.Lazy.UTF8 (fromString)

application counter request = function counter
  where
    function = routes $ pathInfo request

routes path = findRoute path routeSetting

findRoute path [] = notFound
findRoute path ((p,f):xs)
    | path == p = f
    | otherwise = findRoute path xs

routeSetting = [([],                 index),
                (["hello"],          hello),
                (["welcome","world"],world)]

notFound _ = return $
    responseLBS status404 [("Content-type", "text/html")] $ "404 - File Not Found"

index _ = return $
    responseLBS status200 [("Content-type", "text/html")] $ "index page"

hello _ = return $
    responseLBS status200 [("Content-type", "text/html")] $ "hello, my name is Tom"

world counter = do
    count <- lift $ incCount counter
    return $ responseLBS status200 [("Content-type", "text/html")] $
      fromString $ show count

incCount counter = atomicModifyIORef counter (\c -> (c+1, c))

main = do
  counter <- newIORef 0
  run 3000 $ application counter

import

{-# LANGUAGE OverloadedStrings #-}
import Network.Wai
import Network.HTTP.Types (status200, status404)
import Network.Wai.Handler.Warp (run)
import Control.Monad.Trans (lift)
import Data.IORef (newIORef, atomicModifyIORef)
import Data.ByteString.Lazy.UTF8 (fromString)
$ ghci

使用するmoduleをimportしています。

括弧内に関数名を指定することでその関数のみをimportできます。

プログラムの最初に書かれている OverloadedStrings という言語拡張は、ダブルクオートで囲んだ文字列を、ByteString リテラルとして扱ってくれます。 ByteStringは、Stringと比較して効率よく文字列を扱います。

関数の型を確認する

Haskell は型を見れば多くのことが分かる言語です。
ということで、関数の型を見ていきます!

今回作成した例題をロードして、対話モードを開きます。

$ ghci example.hs

型を教えて貰うには、:t コマンドに続けて式を入力します。

ghci> :t run
run :: Port -> Application -> IO ()

run

まず、main内にあるrunから見ていきます。

ghci> :t run
run :: Port -> Application -> IO ()

run は、Port と Application を受け取って、IO () を返す関数だということが分かります。

Haskellのすべての関数は、実は引数を1つだけ取ることになっています。( カリー化関数 )

複数取るように見えますが、実際には関数が1つの引数で呼び出されると、その次の引数を受け取る関数を返します。

これにより、関数を本来より少ない引数で呼び出したときに部分適用された関数が得られます。

IO ()

ghci> :t run
run :: Port -> Application -> IO ()

IO () は IO モナドを表しています。

Haskell では、副作用を持つ処理は基本的に許されていません。

そのため、IO モナドという限られた範囲でのみ行えるようになっています。

IOモナドは外から触ることのできない抽象データ型です。 外から触ることを禁止することで参照透過性を保っています。

Port は、Int の別名です。
別名などの定義は、:t ではみれないので、:i を使うとよいでしょう。
Haskellでは、関数は小文字、型名などは大文字で始まります。

Application

ghci> :i Application
type Application =
  Request -> ResourceT IO Response

Request を受け取って Response を返す関数を表しています。

Portと同じようにtypeで定義されています。

これはRequest -> ResourceT IO Response に別名 Application をつけているという意味です。

Response は 2つのモナドに包まれています。

ResourceT は、IOのリソースの解放を安全に行うためのものです。

run

型情報から以下のことが分かります。

Port 番号と、Request を受け取って Response 返す関数を受け取る
run は IO () を返すので、外界に影響を与える

実際の動作としては、この関数 run は受け取った Port 3000番で、Application を実行します。

次に、Request を受け取って Response を返す関数である application の実装を見ていきます。

application

applicationでは、requestによって呼び出す関数を振り分けます。 where は関数内で使う変数を定義するもので、今回 function には呼び出す関数が入ります。

application counter request = function counter
  where
    function = routes $ pathInfo request

routes の後ろに付いている $ は、関数適用演算子といって括弧の数を減らすのに役たちます。 普通の関数適用は非常に優先順位が高いのですが、$ は最も低い優先順位を持ちます。

($) :: (a -> b) -> a -> b
f $ x = f x

下記の2つのコードは同じ結果になります。

sum (map sqrt [1..130])
sum $ map sqrt [1..130]

request

request には、clientが送る様々な情報が含まれています。
その中には pathInfo という、どこの path へアクセスしてきたかの情報があります。 この情報をroutes関数に渡すことで呼び出す関数を振り分けています。

以下はRequestに含まれる情報です。

request

requestは、data キーワードを使って定義されています。 Haskell では、data キーワードを使って自作のデータ型を作ることができます。

pathInfoという関数は、requestからpathに関する情報を抜き出しますが、これはレコード構文というものを利用しています。

以下にレコード構文の例を表示します。 レコード構文を使うと簡単にアクセサ関数を定義できます。

-- レコード構文を使わない場合
data Person = Person String String Int Float String String
    deriving (Show)

firstName :: Person -> String
firstName (Person firstname _ _ _ _ _) = firstname
lastName :: Person -> String
lastName (Person _ lastname _ _ _ _) = lastname
age :: Person -> Int
age (Person _ _ age _ _ _) = age

( 省略 )

-- レコード構文
data Person = Person { firstName :: String
                     , lastName :: String
                     , age::Int
                     , height :: Float
                     , phoneNumber :: String
                     , flavor :: String } deriving (Show)

routes

routes 関数では、routeSettingを付け足して、findRoute関数を呼び出しています。

routes path = findRoute path routeSetting

routeSetting は、path と関数を記載した List になっています。

routeSetting = [([],                 index),
                (["hello"],          hello),
                (["welcome","world"],world)]

この情報を使って、返す関数を決めます。

findRoute

findRoute path [] = notFound
findRoute path ((p,f):xs)
    | path == p = f
    | otherwise = findRoute path xs

findRoute は再帰的にListを探索しています。

一致するものがなければ、notFound という関数を返します。
一致するものがあれば、routeSetting に記載された関数を返します。

findRoute が二行にわたって書かれているのはパターンマッチを利用しているためです。

findRouteを呼ぶと、パターンが上から下の順で試されます。 渡された値が指定されたパターンと一致すると、対応する関数の本体が使われ、残りのパターンは無視されます。

Response を返す関数の実装

notFoundは、404 - File Not Found と表示する関数になります。 indexは、index page と表示する関数になります。

notFound _ = return $
    responseLBS status404 [("Content-type", "text/html")] $ "404 - File Not Found"

index _ = return $
    responseLBS status200 [("Content-type", "text/html")] $ "index page"

responseLBS とは?

ghci> :t responseLBS
responseLBS
  :: Status
     -> ResponseHeaders
     -> Data.ByteString.Lazy.Internal.ByteString
     -> Response

Statusと、ResponseHeaders、ByteStringを受け取り、Responseを返します。

簡単に説明すると、文字列からResponseを構築するためのコンストラクターです。

Counter の実装

アクセスするたびに、Count がインクリメントされていくようなページを作ります。

ここでは、Thread-safe な State である Data.IORef を用います。

Data.IORefの使い方

IOモナドは中身に直接触ることはできません。

IOモナドからデータを手に入れる唯一の方法は <- を使うことです。
<- を使うことで、純粋なものと不純なものをきっちりと分けています。

変更する際も、modifyIORefなどの関数を利用します。

Data.IORefの使い方

-- IORef Int という型のデータを作製する
counter <- newIORef 0

-- データの更新を atomic に行う
-- atomicModifyIORef には、更新したい IORef a 型の変数と、
-- IORef が持つ値を受け取って 
-- ( 更新後の値, 戻り値にしたい値 ) というタプルを返す関数を渡す
incCount:: IORef a -> IO a
incCount counter = atomicModifyIORef counter (\c -> (c+1, c))

-- 現在のデータの値を受け取る
currentNum <- readIORef counter

Data.IORef を使った Counter の実装

まず、main内でcouterを初期化します。

counter <- newIORef 0

world 関数で、counterを利用します。

world counter = do
    count <- lift $ incCount counter
    return $ responseLBS status200 [("Content-type", "text/html")] $
        fromString $ show count

incCount counter = atomicModifyIORef counter (\c -> (c+1, c))

incCountでは、atomicModifyIORefを使って、counterをインクリメントしています。

また、incCountを、ResourceTのモナド内に持ち込むためliftを行なっています。

Haskell による Web Service

{-# LANGUAGE OverloadedStrings #-}
import Network.Wai
import Network.HTTP.Types (status200, status404)
import Network.Wai.Handler.Warp (run)
import Control.Monad.Trans (lift)
import Data.IORef (newIORef, atomicModifyIORef)
import Data.ByteString.Lazy.UTF8 (fromString)

application counter request = function counter
  where
    function = routes $ pathInfo request

routes path = findRoute path routeSetting

findRoute path [] = notFound
findRoute path ((p,f):xs)
    | path == p = f
    | otherwise = findRoute path xs

routeSetting = [([],                 index),
                (["hello"],          hello),
                (["welcome","world"],world)]

notFound _ = return $
    responseLBS status404 [("Content-type", "text/html")] $ "404 - File Not Found"

index _ = return $
    responseLBS status200 [("Content-type", "text/html")] $ "index page"

hello _ = return $
    responseLBS status200 [("Content-type", "text/html")] $ "hello, my name is Tom"

world counter = do
    count <- lift $ incCount counter
    return $ responseLBS status200 [("Content-type", "text/html")] $
      fromString $ show count

incCount counter = atomicModifyIORef counter (\c -> (c+1, c))

main = do
  counter <- newIORef 0
  run 3000 $ application counter

実行方法

実行方法は2つあります。

$ runghc example.hs

$ ghc --make example.hs
$ ./example

動作確認

シンプルなシステムですが、実際に動くのか確かめてみます。

"index page" と表示されるはず
http://localhost:3000/

"hello, my name is Tom" と表示されるはず
http://localhost:3000/hello

インクリメントされる counter が表示されるはず
http://localhost:3000/welcome/world

一致するpathがないので、"404 - File Not Found" と表示されるはず
http://localhost:3000/hogehoge

Haskell プログラミング

ここまで書いてきて、Haskellのプログラムはだいぶ短く書けることに気がつくと思います。

圧倒的な記述力も特徴のひとつです。

速くて安全なHaskellで、あなたもWeb Serviceを作って見ませんか?

参考文献