elm & servantでwebアプリ - その１．servant導入

## servantの導入

先日、Elm&Haskellによるwebアプリケーションを作成しました。
作り終えてしまうと構築中につまづいたことなどを忘れてしまうため、残しておくことにしました。これからバックエンドにservantを利用してみようと思っている方の一助になれれば幸いです。
最初にアプリケーションルート(/)へのGET要求に対して静的なHTMLを返すWebサーバを作成してみたいと思います。


stackが導入済であることが前提です。
環境は以下の通りです。

- CentOS 7.7
- GHC    8.6.5
- stack  2.1.3


## プロジェクトの作成
webappというディレクトリに
frontend・backendというディレクトリを作成して、それぞれ
frontend : フロントエンドに関わるリソース(html, css, js, ...)
backend  : バックエンドに関わるリソース(DBなど)
と使い分けするように作っていきます。

Servantのプロジェクトをbackendとして作成します。

```
[user@remote ~/webapp]$ stack new backend servant
```
これでServantのテンプレートとなる以下のような構成のプロジェクトが出来上がります。

```
[user@remote ~/webapp]$ tree backend/
backend/
    ├── LICENSE
    ├── README.md
    ├── Setup.hs
    ├── app
    │   └── Main.hs
    ├── backend.cabal
    ├── nohup.out
    ├── src
    │   └── Lib.hs
    ├── stack.yaml
    └── test
        └── Spec.hs

3 directories, 11 files
[user@remote ~/webapp]$
```

### HTMLを返す
まずは、"/"にGETリクエストが来たら以下のHTMLページを返すだけのアプリケーションを作成してみます。

```html:top.html
<!doctype html>
<html>
<head>
    <meta charset="UTF-8">
    <title>test</title>
</head>
<body>
    <h1>hello, servant!!</h1>
</body>
</html>
```
このファイルを`webapp/frontend/top.html`として保存しておきます。

続いて`backend/app/Main.hs`にWebサーバのコードを書いていきます。
先にコードの全量を示します。

```haskell:Main.hs
{-# LANGUAGE DataKinds             #-}
{-# LANGUAGE TypeOperators         #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE MultiParamTypeClasses #-}

import Servant
import Network.Wai.Handler.Warp
import Network.HTTP.Media ((//), (/:))
import qualified Data.ByteString.Lazy as BS

data HTML

instance Accept HTML where
    contentType _ = "text" // "html" /: ("charset", "utf-8")

instance MimeRender HTML BS.ByteString where
    mimeRender _ bs = bs

type API = Get '[HTML] BS.ByteString

api :: Proxy API
api = Proxy

server :: BS.ByteString -> Server API
server top = return top

main :: IO ()
main = do
    top <- BS.readFile "/home/user/webapp/frontend/top.html"
    run 8080 $ serve api (server top)
```

インポートしている以下のモジュールはビルドの依存関係に追加する必要があるので、
webapp/backend/backend.cabalの中にそれぞれ追記します。
※stack.yamlに慣れている方はそちらを編集してください。

- Servant
- Network.Http.Media
- Network.Wai.Handler.Warp
- Data.ByteString.Lazy

```haskell:backend.cabal
executable backend-exe
  hs-source-dirs:      app
  main-is:             Main.hs
  ghc-options:         -threaded -rtsopts -with-rtsopts=-N
  build-depends:       base
                     , backend
                     -- add start
                     , servant-server
                     , http-media
                     , warp
                     , bytestring
                     -- add end
  default-language:    Haskell2010
```



コードの内容を見ていきます。

### GHC拡張
まず先頭のGHC拡張

```haskell
{-# LANGUAGE DataKinds             #-}
{-# LANGUAGE TypeOperators         #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE MultiParamTypeClasses #-}
```

DataKinds拡張はデータ型を種に拡張します。
これによって、型定義にIntなどのデータ型を使用することができます。

TypeOperatorsは型定義で演算子を使用可能にする拡張です。

OverloadedStringsはいちいちpackしなくてもStringをByteStringとしてくれる拡張です。
このコード内では

```haskell
instance Accept HTML where
    contentType _ = "text" // "html" /: ("charset", "utf-8")
```
の部分で"text", "html", "charset", "utf-8"と指定していますが、これらは`//`や`/:`の引数で型はByteStringで定義されています。
それぞれBS.pack "text" などと指定するのは面倒ですが、OverloadedStringsを使えばこの手間は不要です。

最後のMultiParamTypeClassesは
インスタンス定義に複数の型を指定できるようにする拡張です。
`Main.hs`内の

```haskell
instance MimeRender HTML BS.ByteString where
```
で、MimeRender型クラスの種は`k0 -> * -> Constraint`なので型を2つとる必要があります。
そのため、この拡張を有効にしてHTML と BS.ByteStringの２つを指定しています。


###import
割愛


### API定義
```haskell:Main.hs
data HTML

instance Accept HTML where
    contentType _ = "text" // "html" /: ("charset", "utf-8")

instance MimeRender HTML BS.ByteString where
    mimeRender _ bs = bs
```
まず、残念ながらServantにはHTMLという型が定義されてません。
ですが、利用者が簡単にAPIとして作ることができるようになっています。
[ここ](https://hackage.haskell.org/package/servant-0.14.1/docs/Servant-API-ContentTypes.html)に作り方が書いてあるので、これに従って作っています。
>If you would like to support Content-Types beyond those provided here, then:

>    Declare a new data type with no constructors (e.g. data HTML).
>    Make an instance of it for Accept.
>    If you want to be able to serialize data *into* that Content-Type, make an instance of it for MimeRender.
>    If you want to be able to deserialize data *from* that Content-Type, make an instance of it for MimeUnrender.

まず、冒頭に書いてあるとおりデータ型を定義（`data HTML`）します。

>    Declare a new data type with no constructors (e.g. data HTML).

```haskell
data HTML
```

次に作ったデータ型をAcceptのインスタンスにします。

>    Make an instance of it for Accept.

Acceptのインスタンスにするためには、contentType または contentTypesを実装する必要があり、contentTypeの型を調べると

> contentType :: Proxy ctype -> MediaType

となっています。
今回は単にHTMLを返したいだけなので、入力の`Proxy ctype`は使用せずに捨てて
`Content-Type: "text/html":charset=utf-8`をレスポンスヘッダにつけて返すようにしています。

```haskell
instance Accept HTML where
    contentType _ = "text" // "html" /: ("charset", "utf-8")
```


なお、`//`と`/:`の型は以下の通りです。
>(//) :: ByteString -> ByteString -> MediaType
>
>Builds a MediaType without parameters. Can produce an error if either type is invalid.

>(/:) :: MediaType -> (ByteString, ByteString) -> MediaType
>
>Adds a parameter to a MediaType. Can produce an error if either string is invalid.

上記でも述べましたが、ByteStringが入力となっています。


そして
>    If you want to be able to serialize data *into* that Content-Type, make an instance of it for MimeRender.

に従って、MimeRenderのインスタンスにします。mimeRenderを実装すればOKです。

```
instance MimeRender HTML BS.ByteString where
    mimeRender _ bs = bs
```

これでHTMLの定義は完了です。続く

```haskell:Main.hs
type API = Get '[HTML] BS.ByteString

api :: Proxy API
api = Proxy
```
で、このHTMLを返すためのAPIを準備しています。

今回は"/"に対するGet要求にのみ応答するように作っているのでで、`Get '[HTML] ByteString`とだけ記載しています。
HTTPメソッドを処理するためにServantではVerbというデータが定義されています。

```haskell
data Verb method (statusCode :: Nat) (contentType :: [*]) a
```

具体的なメソッドに応じた別名が定義されており、GetもHTTP GETを処理するために別名として定義されています。

```haskell
type Get    = Verb 'GET 200
type Delete = Verb 'DELETE 200
type Patch  = Verb 'PATCH 200
type Post   = Verb 'POST 200
type Put    = Verb 'PUT 200
```
受け付けるHTTPメソッドに応じてこれらを使用することができます。

さて、`Get '[HTML] ByteString`ですが、Verbの定義を見ると型パラメータとして

- method
- statusCode
- contentType
- a

を取ることになっています。
`type Get = Verb 'GET 200`でmethodとstatusCodeは渡されているので、残りを指定してやります。(contentTypeに`'[HTML]`、 aに`ByteString`)
contentTypeは文字通りですが、HTTPレスポンスとして返却するデータの形式を指定します。ここに先ほど定義したデータ型のHTMLを指定してやります。
もしJSONを返したいなら`'[JSON]`と指定します。('[JSON]は用意されています)

aにはAPIに応じた実処理を行うHaskellプログラムが返す型を指定します。
今回はHTMLファイルを読みとって返すので、ByteStringとしています。

### server
```haskell
server :: ByteString -> Server API
server top = return top
```
ここには上記で定義したAPIに対応するHaskellの処理を記載します。
今回は読み込んだHTMLのByteStringをそのまま返すだけなので引数をそのままreturnしています。


### main
```haskell
main :: IO ()
main = do
    top <- BS.readFile "/home/user/webapp/frontend/top.html"
    run 8080 $ serve api (server top)
```
最後、mainです。
まずHTMLファイルを読み込むところは良いかと思います。
run の型は以下の通りです。

> run :: Port -> Application -> IO ()
> 
> Run an Application on the given port. This calls runSettings with defaultSettings.

PortとApplicationを渡せばよいことがわかります。
PortはIntの別名です。
Applicationは以下のように定義されています。

> type Application = Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived
> 
> The WAI application.

> Note that, since WAI 3.0, this type is structured in continuation passing style to allow for proper safe resource handling. This was handled in the past via other means (e.g., ResourceT). As a demonstration:
> 
> app :: Application
> app req respond = bracket_
>     (putStrLn "Allocating scarce resource")
>     (putStrLn "Cleaning up")
>     (respond $ responseLBS status200 [] "Hello World")

Requestと"Responseを受けてIO ResponseReceivedアクションを返す関数"を受け取るようです。
今回のコードでは`serve api (server top)`の部分です。
まず、serveはServant.Serverに定義されており、

>serve :: HasServer api '[] => Proxy api -> Server api -> Application

という定義になっています。確かにApplicationを返します。
Proxy apiにはまさにAPIを渡しており、Server apiの部分には
server topを渡しています。これの型は何だったかというと`server :: ByteString -> Server API`
であり、確かにあっています。serverでreturnすることでAPIを持ったServerを作っているわけですね。

apiとは

```haskell
type API = Get '[HTML] BS.ByteString

api :: Proxy API
```
でした。Proxyは単にAPIを返すだけです。
肝心のAPIは
`Get '[HTML] BS.ByteString`
で前に見た通りGetは`Verb 'GET 200`の別名でした。

## ビルド
`Main.hs`をひととおり見たところでstack buildします。
初回は多数のパッケージ導入が行われるためかなり時間がかかります(私の環境では4時間ほどかかりました)ので、nohup＆バックグラウンド実行することをお勧めいたします。
`nohup stack build --jobs 1 &`

`--jobs 1`は最初メモリが足りず異常終了したため、付け加えました。

無事にビルドが完了したら、`stack exec backend-exe`で実行します。
するとポート8080で待ち受けされます。

別のコンソールを立ち上げてlocalホスト宛にGetを投げてみましょう。

```
[user@remote ~]$ curl -v 'localhost:8080'
* About to connect() to localhost port 8080 (#0)
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET / HTTP/1.1
> User-Agent: curl/7.29.0
> Host: localhost:8080
> Accept: */*
>
< HTTP/1.1 200 OK
< Transfer-Encoding: chunked
< Date: Mon, 25 Nov 2019 13:44:05 GMT
< Server: Warp/3.2.28
< Content-Type: text/html;charset=utf-8
<
<!doctype html>
<html>
    <head>
        <meta charset="utf-8">
        <title>webapp</title>
    </head>
    <body>
        <h1>hello, servant!</h1>
    </body>
</html>
* Connection #0 to host localhost left intact
[user@remote ~]$
```

無事にHTMLが取得されました。レスポンスヘッダのContent-TypeとcharsetもAcceptのインスタンスにする際のcontentType定義が反映されてますね。

次回はElmを導入しtop.htmlをElmで作成してみます。