【Servant】(1) Wai - Qiita
【Servant】(2) Servantチュートリアル - Qiita
【Servant】(3) エンドポイントを増やす - Qiita
【Servant】(4) URLパラメータをハンドラの引数とする - Qiita
【Servant】(5) JSON - Qiita
【Servant】(6) HTML - Qiita
【Servant】(7) Post Data - Qiita
【Servant】(8) Another Monad - Qiita
【Servant】(9) Handlerモナド - Qiita
【Servant】(10) SQLite - Qiita
【Servant】(11) Servant-Client - Qiita
【Servant】(12) Basic 認証 - Qiita
今回はServantチュートリアルとして、公式サイトにある簡単なexampleを動かしてみたいと思います。
1. 少々、前準備
1-1. Servantの特徴
ServantはType(型)レベルでWev APIを記述するDSLです。次のような特徴を持っています。
###(1) concision(簡潔)
同じことを繰り返す必要がない。例えばリソースのserialize や deserializeをその都度マニュアルで行う必要はない。type単位で宣言すればよいだけ。また同じquery parametersを扱うハンドラが複数あったとして、それぞれのハンドラがそのロジックを持つ必要はない。一か所に書けばよい。
###(2) flexibility(フレキシビリティ)
それぞれの要望において、必要なものを必要な時に追加できる。
###(3) separation of concerns(関心の分離)
リクエスト・ハンドラとHTTP ロジックが分離されている。
###(4) type safety
APIの仕様を満たしていることをコンパイラが保証してくれる。
1-2. GHCの型拡張に親しむ
Servantの一番の特徴は、APIをtypeとして定義することです。逆にこの辺が、私のような初心者には違和感を覚えるところです。この違和感を拭うためには、少しGHCの型拡張に慣れる必要があります。後の説明でも簡単に触れますが、以下のようなサイトを、できる範囲で参照しておくことが望まれます。
About kind system of Haskell (Part 1)
Haskellの種(kind)について (Part 2)
Haskellにおける型レベルプログラミングの基本(翻訳)
Part I: Dependent Types in Haskell
1-3. Servantのドキュメントを読む
Servant自身のドキュメントは以下にあります。
servant – A Type-Level Web DSL - 公式サイト
servant-server: A family of combinators for defining webservices APIs and serving them - hackage
2.最初のプロジェクト
今回は、公式サイトの最も簡単なexampleを動作させることを目指します。
Docs » Tutorial » Serving an API - A first example
2-1. プロジェクトを作る
まずはstackプロジェクトを作成します。
stack new first-project
cd first-project
動作を確認します。
stack build && stack exec first-project-exe
以下のような出力が得られます。
someFunc
stackプロジェクトの、最初のソースを確認しておきます。
メインは以下の通りです。
module Main where
import Lib
main :: IO ()
main = someFunc
メインから以下の関数が呼ばれます。
module Lib
( someFunc
) where
someFunc :: IO ()
someFunc = putStrLn "someFunc"
2-2. はじめてのServant
それではソースを変更して、Servantアプリに変更します。
メインでは呼び出す関数名をrunServantに変更します。
module Main where
import Lib
main :: IO ()
main = runServant
Libs.hsを以下のように変更します。オリジナルのソースでは言語拡張宣言やimport文が鬼のようについていますが、必要最小限のものに削っているのでスッキリしています。
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE DeriveGeneric #-}
module Lib
( runServant
) where
import Servant(serve, Proxy(..), Server, JSON, Get, (:>))
import Data.Aeson(ToJSON)
import Data.Time.Calendar
import GHC.Generics(Generic)
import Network.Wai(Application)
import Network.Wai.Handler.Warp(run)
data User = User
{ name :: String
, age :: Int
, email :: String
, registration_date :: Day
} deriving (Eq, Show, Generic)
instance ToJSON User
users1 :: [User]
users1 =
[ User "Isaac Newton" 372 "isaac@newton.co.uk" (fromGregorian 1683 3 1)
, User "Albert Einstein" 136 "ae@mc2.org" (fromGregorian 1905 12 1)
]
type UserAPI1 = "users" :> Get '[JSON] [User]
server1 :: Server UserAPI1
server1 = return users1
userAPI :: Proxy UserAPI1
userAPI = Proxy
-- serve allows you to implement an API and produce a wai Application.
-- serve :: HasServer api '[] => Proxy api -> Server api -> Application
app1 :: Application
app1 = serve userAPI server1
runServant :: IO ()
runServant = run 8081 app1
必要なライブラリを追加します。
dependencies:
- base >= 4.7 && < 5
- servant-server
- aeson
- time
- wai
- warp
コンパイルして、実行してみます。
stack build && stack exec first-project-exe
curlコマンドでアクセスしてみます。
$ curl http://localhost:8081/users
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 188 0 188 0 0 858 0 --:--:-- --:--:-- --:--:-- 870
[{"email":"isaac@newton.co.uk","registration_date":"1683-03-01","age":372,"name":"Isaac Newton"},
{"email":"ae@mc2.org","registration_date":"1905-12-01","age":136,"name":"Albert Einstein"}]
望み通りの結果が得られました。満足です。
2-3. 説明
2-3-1. WAI & Warp
前回のWarpのrunコマンドの復習です。
type Port = Int
run :: Port -> Application -> IO ()
2-3-2. Application
runコマンドで走らせるApplicationは以下のように定義されます。
-- serve allows you to implement an API and produce a wai Application.
-- serve :: HasServer api '[] => Proxy api -> Server api -> Application
app1 :: Application
app1 = serve userAPI server1
serveでApplicationを生成します。serveの型は以下の通りです。
serve :: HasServer api '[] => Proxy api -> Server api -> Application
つまりserveは API(userAPI)とハンドラ(server1)を結び付けてApplicationを返しています。API(userAPI)とハンドラ(server1)は以下のように別々に定義されています。
server1 :: Server UserAPI1
server1 = return users1
userAPI :: Proxy UserAPI1
userAPI = Proxy
ProxyはUserAPIという型を保持しています。
2-3-3. 型とハンドラ
Servantの骨子は、型として記述されたAPIと、ハンドラです。そして重要なのはそのAPIとハンドラが分離されていることです。
APIの定義は以下のように型レベルで行われます。型としてHTTP のGETメソッドが定義されています。エンドポイント "users"へのHTTP GETリクエストがあった場合、User型のリストをJSONで返すことを定義しています。
type UserAPI1 = "users" :> Get '[JSON] [User]
リストの前についたアポストロフィ '[JSON] が違和感がありますが、これは DataKinds によって導入されたものです。通常のカギカッコ[...]は、リストを表現するための値構成子ですが、DataKinds によって**'**[....]は型構成子に昇格させられています。Heterogenous Listsのようなものです。
また**:>もGHCの型拡張のTypeOperators**で、型構成子として使われています。
UserAPI1に対応するハンドラは以下のように定義されています。皆さん大好きなモナドですね。
server1 :: Server UserAPI1
server1 = return users1
2-3-4. JSON シリアライズ
Webサーバ(Servant)とクライアント間では、User型のデータを、JSON形式で、やりとりすることになりますが、もちろんAesonを利用します。GHCの言語拡張DeriveGenericを用いて以下のように宣言するだけです。
---
{-# LANGUAGE DeriveGeneric #-}
---
import GHC.Generics(Generic)
---
data User = User
{ name :: String
, age :: Int
, email :: String
, registration_date :: Day
} deriving (Eq, Show, Generic)
instance ToJSON User
今回は以上です。