この記事の目的
- Haskell の Web フレームワーク Scotty に登場する ActionM の正体を理解する
- ActionM の使い方を、具体例付きで実感する
- Haskell で「Webアプリってどう書くの?」という疑問に答える
ScottyのActionMって何?
Haskell の Web開発用フレームワーク Scotty。
そこで登場する型(型コンストラクタ)がActionMです。
一言でいうと、
「Webリクエストに対する処理を書くための“箱(モナド)”」
です。
Webアプリでは、
- ユーザーからリクエストが来て
- 処理して
- レスポンスを返す
という流れがあります。
この「一連の流れ」を書くための場所、それが ActionM です。
※モナドについて
ここでは、厳密な話はしません。正確ではないですが、外部に影響を与えないようにするための箱、のようなイメージを持ってみてください。
どこで出てくる?
よくある例
get "/hello" $ do
json $ object ["message" .= ("Hello!" :: String)]
ここで do ブロックの中身はすべて ActionM () 型です。
つまり、
- 「GET /hello」というリクエストに対して
- 「JSONを返す」というアクションを
- ActionM の中で実行している
ActionM の中で使える主な関数
関数 | 用途 | 例 |
---|---|---|
param |
URLのパラメータ取得 |
param "id" で /memos/:id の値を取得 |
body |
POSTされたリクエストボディを取得 | JSON文字列などを受け取る |
json |
JSONを返す | json memo |
status |
HTTPステータスコードを設定 | status status404 |
liftIO |
IO処理を使う | DBアクセスなどで使用 |
具体例つきで学ぶ
例1: 単純なレスポンスを返す
get "/hello" $ do
json $ object ["message" .= ("Hello, world!" :: String)]
/hello
にアクセスした人に、JSON形式でメッセージを返すだけの処理です。
例2: パラメータ付きのレスポンス
get "/hello/:name" $ do
name <- param "name"
json $ object ["message" .= ("Hello, " ++ name ++ "!")]
/hello/Alice
にアクセスすると {"message": "Hello, Alice!"}
が返ります。
例3: POSTされたデータを受け取って保存
post "/echo" $ do
b <- body
text b
リクエストボディをそのまま返すだけのエコーサーバー。
ActionM の裏側 ActionT
Scottyの内部では、ActionM は以下のように定義されています。
type ActionM = ActionT IO
つまり、ActionTによって定義されています。
ActionTの定義を見てみましょう。
-- 簡略版の定義
newtype ActionT m a = ActionT { runActionT :: Request -> m (Response, a) }
-
ActionT
は モナド変換子(Monad Transformer) です。 - 型パラメータ
m
には、通常IO
などのモナドが入ります。 -
a
は「処理の結果として返す値の型」です。 -
Request -> m (Response, a)
という関数をラップしています。
つまり、「HTTPリクエストを受け取り、レスポンスを生成するような処理(副作用あり)を表現するモナド」を意味しています。
例えば、ActionT IO String
ならば、IO
を使って HTTP
リクエストを処理し、最終的に String
を返す ActionT
モナド、という意味です。
ActionMの定義に戻りましょう。
type ActionM = ActionT IO
- ActionM は ActionT に IO を固定した型のエイリアス(別名) です。
- つまり、「IOモナドで動く ActionT」ということです。
IO モナドについて 補足
Haskellは純粋関数型言語なので、「副作用(=外部世界への影響)」は直接書けません。ですがWebアプリでは、
- ユーザーの入力を受け取る
- データベースを操作する
- ファイルを読み書きする
といった副作用は必須です。
そのため、こうした副作用を「安全な箱」で包むのが IO モナドです。
実用:メモの作成処理(POST)
post "/memos" $ do
b <- body
case eitherDecode b :: Either String NewMemo of
Left err -> do
status status400
json $ object ["error" .= ("Invalid JSON: " ++ err)]
Right newMemo -> do
liftIO $ saveToDB newMemo
status status201
json newMemo
- body でリクエストボディを取り出し
- eitherDecode で JSON を解析
- エラーなら 400 を返し
- 成功したら DB に保存し、201 を返す
という処理を ActionM の中で書いています。
※例として、メモはNewMemo
型で管理しています。
補足 getの型は?
get "/hello" $ do
json $ object ["msg" .= ("Hello!" :: String)]
このgetの型も確認しておきます。
getの型
get :: RoutePattern -> ActionM () -> ScottyM ()
「指定したパスに対して、HTTP GET リクエストが来たときの処理(ActionM
)を登録する処理(ScottyM
)」です。
RoutePattern型とは?
リクエストの パスにマッチするための「パターン」の型
ActionM ()の()とは?
()
は、「何も返さない」ことを表す特別な型です。
Webリクエストに対して何らかの処理を行うけど、処理の結果(値)は外に返さない。副作用だけを行う。
ScottyM ()
これはルーティングを定義する Scottyアプリ全体の設定を構築するためのモナド。
get
は「このルーティングを1つ追加する」という副作用的な処理なので、結果は ScottyM ()
型になります。
関連記事
https://qiita.com/meta77/items/3e62724bb4a4f4ac9541
https://qiita.com/meta77/items/91e09025338626fa220b
https://qiita.com/meta77/items/6d6bbbc3abc4b7bd8338