読者です 読者をやめる 読者になる 読者になる

くりふぃの日記

プログラミングとかのメモ書きとかに使えたらいいなぁ。

servantで簡単WebAPI

Haskell servant

最近はもっぱらHaskellの勉強中で、最近ようやく簡単なモナド変換子の取り扱いにも慣れてきたところで、 WebAPIの実装練習をしてみようかなと使ってみたservantが非常に使い勝手がよかったので紹介記事を書きました。

servantとは

servantは型レベルでWebAPIを書けるライブラリである。 WebAPIの定義を型レベルで宣言することで、APIサーバーのルーティングなどを型安全に書くことができる。

servantでAPIサーバーを実装するにあたってwaiを使用することになる。

servantの使い方

servantではまずAPIの型を定義する必要がある。この型は型演算子を用いて定義でき、一種のDSLのようになっている。

その型を用いて関数を定義することで簡単にAPIサーバーを実装することが可能となっている。

例えば/usersのようなURLのAPIを定義し、実装をすると以下のようなコードとなる。

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeOperators #-}

module Main where

import Data.Aeson as A
import Data.Aeson.TH as ATH
import Network.Wai as W
import Network.Wai.Handler.Warp as Warp
import Servant as S

data User = User { userId:: Int, userFirstName :: String, userLastName :: String } deriving Show
$(ATH.deriveJSON A.defaultOptions ''User)

-- /users
type API = "users" :> S.Get '[S.JSON] [User]

main :: IO ()
main = Warp.run 8080 app

app :: W.Application
app = S.serve api server

api :: S.Proxy API
api = S.Proxy

server :: S.Server API
server = return users

users :: [User]
users = [User 1 "firstname1" "lastname1", User 2 "fistname2" "lastname2"]

これだけでビルドして実行すればAPIサーバーとしてリクエストを受け取りUserのリストをJSONでレスポンスしてくれる。 型演算子:>がURLのスラッシュを表していると考えるとわかりやすい。

Get '[JSON] UserについてはGETでUserをJSONで返却するという意味になっている。 JSON処理にAesonを使用していてAesonのToJSONのインスタンスである型の値を渡すことでservant側で勝手に変換してくれる。

このため、処理は単純な関数として実装でき、最終的に必要な値を返却すればよいので非常にシンプルである。

API一つの処理を関数はExceptT ServantErr IO aという型である必要がある。

例外処理を書かなければ単にIO aの関数を書いてliftIOで持ち上げるだけとなるのでHaskellで副作用を取り扱えるのであれば難しい要素は一切ない。

※副作用もなければ例示したコードのように値をreturnでモナドに包んでやるだけである。

エラー処理に関してもExceptTでServantErrを使用する必要があるというだけであるため、Haskellで例外の取り扱いが分かっているのでのあれば特に複雑な構造というわけでもないはずである。

これだけ簡単に型安全なAPIサーバー実装が行えるのだからservantはとても強力なライブラリであるといえよう。

複数のAPI

しかし、APIサーバーは当然1つのURLではなく複数のURLを持つものである。また、URLの一部をパラメータとして利用するケースも多いにある。

この定義に追加してURLにIDを含んだ形で受け取りその値を利用して一つのUserの値を取得して返却するAPIを定義するにはどのようにすべきか。

答えは以下のようにtypeの定義を変えた上でserverに処理を追加してあげればよい。

-- 変更・追加部分のみ

-- GET /users
-- GET /users/:id
type API = "users" :> S.Get '[S.JSON] [User]
    :<|>   "users" :> S.Capture "id" Int :> S.Get '[S.JSON] User

-- (略)

server :: S.Server API
server = return users
    :<|> return user

-- (略)

-- 単純化するためCaptureで取得した値を捨てている
user :: Int -> User
user _ = User 1 "firstname1" "lastname1"

:<|>演算子によって複数のAPIを持つ型を定義できる。処理についても型と同じように:<|>演算子を使って繋げてあげるだけよい。

そしてCapture "id" Intという記述でその部分がパラメータとして利用できるようになる。 Captureの第二引数として型を定義しているため、それを受け取り処理する関数の型も決まるため型安全に処理を記述することができる。

3つ以上でも考え方は同じであり、クエリ文字列の取得、POSTのリクエストボディの取得についても同じように定義できる。

詳しくはservantのドキュメントHackageを見て実際にコードを書いてみてもらいたい。