HaskellとScottyで簡単にAPIを構築する

# はじめに
HaskellのWebフレームワークはYesod、Spockなどいくつかありますが、そのなかでも軽量なScottyを紹介します。
RubyのSinatraに影響を受けており、非常に簡単にWebサーバを作ることができます。

# 準備
以下のライブラリを使いますので、`package.yaml`などに追加しておいてください。

```yaml
- http-types
- aeson
- scotty
```

# まずとても簡単な例
以下のようなコードを書いて実行してみましょう。たった5行でサーバを立てることができます。

```hs:Sample.hs
{-# LANGUAGE OverloadedStrings #-}
module Sample where
import Web.Scotty
main :: IO ()
main = scotty 3000 $ get "/" $ html "<h1>Hello</h1>"
```

`http://localhost:3000/`にアクセスしてみましょう。

# ユーザのAPI
例としてユーザデータを参照/作成/削除をするAPIを実装してみます。簡単のためにDBなどは使わずにオンメモリにデータを保存することにします。

## ユーザデータの定義
まず、JSONで扱えるユーザデータを定義します。また、参照/作成/削除の関数も実装しておきます。

```hs
data User = User { uid :: Integer, name :: String, age :: Integer } deriving (Generic, Show)
instance ToJSON User
instance FromJSON User

addUser :: [User] -> User -> [User]
addUser users user = user:users

deleteUser :: [User] -> Integer -> [User]
deleteUser users i = filter (\user -> uid user /= i) users

findUser :: [User] -> Integer -> Maybe User
findUser users i = find (\u -> uid u == i) users
```

## エラーの定義
```hs
data Error = Error { message :: String } deriving (Generic, Show)
instance ToJSON Error
instance FromJSON Error
```

## ユーザの簡易のリポジトリ
Listでユーザのリポジトリを作成します。今回はListの中身を変更できるように`IORef`を使います。
`IORef`は例外的にミュータブルな（=変更可能な）変数を扱うためのモナドです。
`readIORef`で`[User]`型データを読み込んだり、`writeIORef`で書き換えができます。

```hs
main = do
  users <- newIORef [] :: IO (IORef [User])
```

## Scottyの起動
`scotty`関数を呼び出すことで起動します。次の例ではポート3000番でlistenします。

```hs
main = do
  -- (省略)
  scotty 3000 $ do
```


## ユーザの取得
すべてのユーザを取得するAPIを作成します。
`liftIO`については[こちら](https://qiita.com/7shi/items/4408b76624067c17e933#%E5%A4%9A%E9%87%8D%E6%8C%81%E3%81%A1%E4%B8%8A%E3%81%92)を参照してみてください。

```hs
get "/users" $ do
  us <- liftIO (readIORef users) -- リポジトリからすべてのユーザを取得
  status status200
  json us
```

```bash:呼び出し例
$ curl -X GET http://localhost:3000/users
```

さらにPathパラメータ`:uid`を利用して、特定のユーザを取得するAPIも作成します。

```hs
get "/users/:uid" $ do
  us <- liftIO (readIORef users)
  i <- param "uid" -- Pathパラメータ uid を取得
  case findUser us (read i) of -- 指定されたユーザが存在するか否かで場合分け
    Just u -> status status200 >> json u
    Nothing -> status status404 >> json (Error ("Not Found uid = " <> i))
```

```bash:呼び出し例
$ curl -X GET http://localhost:3000/users/1
```

## ユーザの作成
ユーザを作成するAPIを作成します。

```hs
post "/users" $ do
  u <- jsonData -- BodyをJSONで取得
  us <- liftIO $ readIORef users
  liftIO $ writeIORef users $ addUser us u -- 新規ユーザが追加されたリストにリポジトリを書き換え

  status status201
  json u
```

```bash:呼び出し例
$ curl -X POST http://localhost:3000/users -d '{ "uid": 1, "name": "alice", "age": 20 }'
```

## ユーザの削除
ユーザを削除するAPIを作成します。

```hs
delete "/users/:uid" $ do
  i <- param "uid"
  us <- liftIO $ readIORef users
  liftIO $ writeIORef users $ deleteUser us i
  status status204
```

```bash:呼び出し例
curl -v -X DELETE http://localhost:3000/users/1
```

## 完成コード
完成したコードは以下のようになります。Scottyを使うと割と簡単にAPIを記述できました！

```hs:UserAPI.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveGeneric #-}

module UserAPI where

import Web.Scotty
import Data.Aeson (FromJSON, ToJSON)
import GHC.Generics
import Data.IORef
import Control.Monad.Reader
import Network.HTTP.Types.Status
import Data.List (find)

data User = User { uid :: Integer, name :: String, age :: Integer } deriving (Generic, Show)
instance ToJSON User
instance FromJSON User

data Error = Error { message :: String } deriving (Generic, Show)
instance ToJSON Error
instance FromJSON Error

addUser :: [User] -> User -> [User]
addUser users user = user:users

deleteUser :: [User] -> Integer -> [User]
deleteUser users i = filter (\user -> uid user /= i) users

findUser :: [User] -> Integer -> Maybe User
findUser users i = find (\u -> uid u == i) users

main :: IO ()
main = do
  users <- newIORef [] :: IO (IORef [User])
  scotty 3000 $ do
    get "/users" $ do
      us <- liftIO (readIORef users)
      status status200
      json us
    get "/users/:uid" $ do
      us <- liftIO (readIORef users)
      i <- param "uid"
      case findUser us (read i) of
        Just u -> status status200 >> json u
        Nothing -> status status404 >> json (Error ("Not Found uid = " <> i))
    post "/users" $ do
      u <- jsonData
      us <- liftIO $ readIORef users
      liftIO $ writeIORef users $ addUser us u
      status status201
      json u
    delete "/users/:uid" $ do
      i <- param "uid"
      us <- liftIO $ readIORef users
      liftIO $ writeIORef users $ deleteUser us i
      status status204
```