はじめに
APIハンドラの実装では、複数のハンドラで共通に使う処理を一つの関数として実装し、それを使いまわす、というシーンも多くなります。一般的なプログラミング言語では「単純に関数を定義すれば終わり」となる話題ですが、Haskellの場合には、共通処理の関数を使う場面によって、いくつか注意点があります。
今回の記事で記載する内容は、そんなにディープな話題ではないのですが、とはいえこういった話題に触れるような記事もほとんどないようなので、ここでまとめてみたいと思います。
共通処理の例
例えば、「指定したPersonを返す」というAPIと「指定した条件のPersonのリストを返す」というAPIがあるとします。RDBのアクセスを含めて、それぞれ別に実装するのもいいのですが、いろんな機能を実装していくと「似たような処理を別々に実装する」となり、保守性が下がっていきます。この例であれば「指定した条件のPersonのリストを返す」ような関数を実装して、これを両方のAPIで使用することで、保守性の低下を防ぐことができます。
共通処理の型定義
共通処理を呼び出す場所による違い
共通処理の関数を呼び出す「場所」によって、共通関数の型がかわります。その違いが出るのは「runSqlの外か中か」になります。「APIハンドラ内でrunSqlの外」であれば、APIハンドラ自体の型と変わりません。この記事シリーズであれば「MyAppHandler a 型」となります。
問題は、runSql配下(doブロックがあってもなくても)の「中」の場合です。この中では特に何をしなくても「RDBへのアクセスができる場所」となりますので、共通処理関数の型や実装のやり方が変わります。ほとんどのケースで共通処理はこちらのパターンの利用になりますので、こちらの整備が重要です。
型定義・ステップ1(runSqlのみサポート)
runSqlはrunReaderTやrunResourceTが含まれるので、型もそれに相当した「SqlPersistT (ResourceT IO) a」となります。例えば、共通処理の例として、PersonIdのリストからPersonのリストを返す関数getPerson'は下記のようになります。注目点は、この関数内ではrunSqlが不要、ということです。仮にrunSqlを誤って入れてしまった場合は、型が合わないのでビルドエラーになります。なお、呼び出し元でerrorHandlerを使っていれば、共通関数内で投げた例外も呼び出し元側で拾ってもらえます。そのため、例外を投げる方法については、APIハンドラでの方法と同じ方法でOKです。
-- 共通処理関数の例。型は「SqlPersistT (ResourceT IO) a」
getPerson' :: [PersonId] -> SqlPersistT (ResourceT IO) [Entity Person]
getPerson' pid_list = do
-- runSql抜きにRDBアクセスできる
select $ from $ \p -> do
where_ $ p ^. PersonId `in_` valList pid_list
return p
これを利用して、getPersonやgetPersonListからは、下記のようgetPerson'を呼び出す形になります。
getPerson :: PersonId -> MyAppHander Person
getPerson pid = errorHandler $ runSql $ do
-- runSql配下で共通処理関数を呼べる
entity_person_list <- getPerson' [pid]
-- entity_person_listから戻り値を生成する処理をここに実装
getPersonList :: [PersonId] -> MyAppHander [Person]
getPersonList pid_list = errorHandler $ runSql $ do
-- runSql配下で共通処理関数を呼べる
entity_person_list <- getPerson' pid_list
-- entity_person_listから戻り値を生成する処理をここに実装
型定義・ステップ2(runSql+MyAppConfigサポート)
ステップ1で共通処理の実装ができたのですが、もうひと拡張すると、さらに便利になります。
先の記事にて、ServantのHandlerに「ReaderT MyAppConfig」を適用してMyAppHandlerに変換することで、APIハンドラ内でMyAppConfigの値にアクセスできることを書きました。
-- MyAppConfig型の値へのアクセスの例(再掲)
getPerson :: PersonId -> MyAppHander Person
getPerson pid = errorHandler $ do
app_flag <- asks getApplicationFlag -- MyAppConfigの値
runSql $ do
when app_flag $ do
-- MyAppConfigのFlagが立っている時の処理
ところが、ステップ1の実装では、runSql配下ではMyAppHandlerとは型が違っていますので、MyAppConfig型の値へのアクセスができません。そのため、共通処理の関数内でも同様にMyAppConfigの値にはアクセスできません。MyAppConfig型の値を共通処理関数の引数に入れてもいいのですが、関数のネストが深くなったりすると、結構面倒になります。そのため、runSqlの関数を下記のように修正し、新しい型として名前は何でも構わないのですが、ここでは「SqlPersistM'」を定義します。
-- 共通関数用のモナド型定義
type SqlPersistM' = SqlPersistT (ResourceT (ReaderT MyAppConfig IO))
-- runSql修正
runSql :: (MonadReader MyAppConfig m, MonadIO m) => SqlPersistM' b -> m b
runSql query = do
config <- ask -- MyAppConfig型の値を取得して、runReaderTで使う
pool <- asks getPool -- コネクションプール
liftIO $ flip runReaderT config $ runResourceT $ runSqlPool query pool
MyAppHandlerの代わりに、このSqlPersistM'型を用いた共通処理関数でも、MyAppConfig型の値にアクセス可能です。runSqlが不要なのは、先の話と同じです。
getPerson' :: [PersonId] -> SqlPersistM' [Entity Person]
getPerson' pid_list = do
app_flag <- asks getApplicationFlag -- ここでもMyAppConfig型の値にアクセスできる
when app_flag $ do
-- app_flagが立っている時の処理
まとめ
「たかだか共通処理を実装するだけで、型変換などが必要」というと、「Haskellは、なんて面倒くさい」という話もあるかもしれないですが、その裏返しとして「関数の使い方を間違えたためのランタイムエラーなどが出ない(出づらい)」というメリットがあります。
今回の話題は、ServantとPersistentの組み合わせの話題なので、なかなか見ないような記事ですが、実用WebAPIの実装としてはかなり基本的なスキルになります。
リポジトリに、ステップ2の型定義の実装をコミットしました。