YAMAMOTO Yuji (山本悠滋)
2021-11-07 Haskell Day 2021
同じ目的であるSlack Archiveに対して:
slack-log save
:
slack-log generate-html
:
cabalファイルを見てみよう
slack-log
という名前の実行ファイルのソースコードはapp
にあって、その中のMain.hs
にmain
関数がある
Main.hs
を先頭から読んで、最初にSlackのWeb
APIを呼び出すところまでまずはapp/Main.hs
を見てみよう:
import UI.Butcher.Monadic ({- ... 省略 ... -})
main :: IO ()
main = do
-- ... 省略 ...
mainFromCmdParserWithHelpDesc $ \helpDesc -> do
addHelpCommand helpDesc
addCmd "save" $ addCmdImpl saveCmd
addCmd "generate-html" $ do
-- ... 省略 ...
addCmdImpl $ generateHtmlCmd onlyIndex
addCmd "paginate-json" $ addCmdImpl paginateJsonCmd
slack-log save
この行で、サブコマンドsave
を追加
slack-log save
コマンドはここで設定saveCmd
という関数が実際に実行するIO
アクションsaveCmd
関数(設定の読み込み その1)saveCmd
関数(設定の読み込み その2)saveCmd = do
-- ... --
apiConfig <- Slack.mkSlackConfig . slackApiToken =<< failWhenLeft =<< decodeEnv
-- ... --
saveCmd
関数(設定の読み込み その3)saveCmd
関数(ユーザー一覧の保存)saveUsersList
関数でSlackのWeb
APIを呼び出して、Workspaceにいるすべてのユーザーの情報を取得・保存
saveUsersList
関数saveUsersList :: ReaderT Slack.SlackConfig IO ()
saveUsersList = do
result <- Slack.usersList
-- ... result からユーザーの一覧を取り出して保存
Slack.usersList
という関数が実際にSlackのWeb
APIを呼ぶ関数
Slack.usersList
という名前で参照されているが、正式にはusersList
という名前ReaderT
型・runReaderT
関数 (1)先に結論:
runReader
関数で渡した引数を、do
の中に書いたあっちこっちの関数に自動的に渡してくれるReaderT
型・runReaderT
関数 (2)今回紹介した箇所では次のような型に:
runReaderT :: ReaderT r IO a
-> r -> IO a
usersList :: ReaderT r IO (Response ListRsp)
saveUsersList :: ReaderT r IO ()
runReaderT
はr
(slack-logではSlackConfig
が該当)を受け取ることで、「ReaderT r IO
」という(変な名前の)Monad
をただのIO
に変換するReaderT
型・runReaderT
関数 (3)今回紹介した箇所では次のような型に:
runReaderT :: ReaderT r IO a
-> r -> IO a
usersList :: ReaderT r IO (Response ListRsp)
saveUsersList :: ReaderT r IO ()
usersList
を含め、SlackのAPIを実行する関数は、ReaderT r IO
というMonad
の関数になっている
saveUsersList
はusersList
を呼ぶため、同様にReaderT r IO
というMonad
にMonad
の関数を呼ぶには呼び出し元の関数も同じMonad
の関数である必要があるReaderT
とは?Monad
の機能を一つのdo
記法で使えるよう、合成したMonad
を作る
ReaderT r IO
」の場合「Reader r
」の機能とIO
の機能が同時に使えるIO
はおなじみ。入出力を始めなんでもできる。では「Reader r
」とは?Reader
とは?実は「Reader r a
」は「r -> a
」と等価な(newtype
した)もの:
do
で扱えるようにしただけ!Reader r
」はどうやって関数をMonadに?
do
記法で使ったとき何が起こるか・何ができるかを知るといいReader
のdo
(を使わなかった場合)例:
「Reader r
」のdo
を使う前のただの関数
Reader
のdo
(を使った場合)例: 「Reader r
」のdo
を使った関数
someFunc :: Reader Arg [Result]
someFunc = do
r1 <- f1
r2 <- f2
r3 <- f3
return $ r1 ++ r2 ++ r3
-- 使う時は runReader を使ってこう👇
runReader someFunc arg
arg
が、runReader
関数を呼ぶときに一度だけ渡せば良くなってる!Reader
がdo
でしていることrunReader
関数で渡した引数arg
を、あっちこっちの関数に自動的に渡してくれる。それだけ!
arg
と書いて渡せば良い話でもある。見かけの問題usersList
関数👆は結局のところ👇と実質同じ
ReaderT
を使っているのかusersList
関数のような、SlackのAPIをたくさん呼ぶアプリケーションを書くときにSlackConfig
をあちこちの関数に逐一渡さなくて良くなる
ReaderT SlackConfig IO
が利用できる箇所では利用ReaderT r IO
は、数あるMonad
Transformerの用途の中で最も知られている、「ReaderT
パターン」を実現するのに用いられる
usersList
を始めslack-webパッケージの各種APIを呼ぶための関数も「ReaderT
パターン」で使いやすくなるよう型付けされていると思われる
ReaderT
パターン」については割愛!ReaderT
を使うべきか?ReaderT SlackConfig IO a
とSlackConfig -> IO a
間の相互変換は充分に簡単なので、初めて使う人が混乱しないよう、敢えてライブラリー側でReaderT
の利用を強要しないで欲しい
実際のusersList
の型:
超単純化したusersList
の型:
「ユーザー一覧の保存」をしている箇所(再掲)
runReaderT
やReaderT
によって
が、
になって
runReaderT
やReaderT
によって
saveUsersList :: Slack.SlackConfig -> IO ()
saveUsersList apiConfig = do
result <- Slack.usersList apiConfig
-- ... result からユーザーの一覧を取り出して保存
が、
saveUsersList :: ReaderT Slack.SlackConfig IO ()
saveUsersList = do
result <- Slack.usersList
-- ... result からユーザーの一覧を取り出して保存
になる!
ReaderT
パターンがより適用しやすくなる!ReaderT
Monad
Transformerの使い方を押さえておこう!
(`runReaderT` apiConfig) someApiFunc
というイディオムを使えば良い