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 paginateJsonCmdslack-log saveこの行で、サブコマンドsaveを追加
slack-log saveコマンドはここで設定saveCmdという関数が実際に実行するIOアクション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を使う前のただの関数
someFunc :: Arg -> [Result]
someFunc arg =
let r1 = f1 arg
r2 = f2 arg
r3 = f3 arg
in r1 ++ r2 ++ r3
-- 使う時は単純にこう👇
someFunc argReaderのdo(を使った場合)例: 「Reader r」のdoを使った関数
someFunc :: Reader Arg [Result]
someFunc = do
r1 <- f1
r2 <- f2
r3 <- f3
return $ r1 ++ r2 ++ r3
-- 使う時は runReader を使ってこう👇
runReader someFunc argargが、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の型:
「ユーザー一覧の保存」をしている箇所(再掲)
saveUsersList :: ReaderT Slack.SlackConfig IO ()
saveUsersList = do
result <- Slack.usersList
-- ... result からユーザーの一覧を取り出して保存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というイディオムを使えば良いSpace, Right Arrow or swipe left to move to next slide, click help below for more details