7
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【Servant】(2) Servantチュートリアル

Last updated at Posted at 2020-02-15

【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の特徴

Introduction - 公式サイト

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プロジェクトの、最初のソースを確認しておきます。

メインは以下の通りです。

app/Main.hs
module Main where

import Lib

main :: IO ()
main = someFunc

メインから以下の関数が呼ばれます。

Lib.hs
module Lib
    ( someFunc
    ) where

someFunc :: IO ()
someFunc = putStrLn "someFunc"

2-2. はじめてのServant

それではソースを変更して、Servantアプリに変更します。

メインでは呼び出す関数名をrunServantに変更します。

app/Main.hs
module Main where

import Lib

main :: IO ()
main = runServant

Libs.hsを以下のように変更します。オリジナルのソースでは言語拡張宣言やimport文が鬼のようについていますが、必要最小限のものに削っているのでスッキリしています。

Lib.hs
{-# 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

必要なライブラリを追加します。

package.yaml
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の型は以下の通りです。

Servant.Server - hackage

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

今回は以上です。

7
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?