モナドの上にまたモナドって株か?二階建てか?なんて思って敬遠してたのだけど、なんとなくわかってくると、包んで包んでしてるだけじゃないかと。
RWH18章
モナド変換子を使わないでログ記録。これはIO [(FilePath, Int)]型になる。
module CountEntries (listDirectory, countEntriesTrad) where import System.Directory (doesDirectoryExist, getDirectoryContents) import System.FilePath ((</>)) import Control.Monad (forM, liftM, when) listDirectory :: FilePath -> IO [String] listDirectory = liftM (filter notDots) . getDirectoryContents where notDots p = p /= "." && p /= ".." countEntriesTrad ::FilePath -> IO [(FilePath, Int)] countEntriesTrad path = do contents <- listDirectory path rest <- forM contents $ \name -> do let newName = path </> name isDir <- doesDirectoryExist newName -- when isDir $ countEntriesTrad newName if isDir then countEntriesTrad newName else return [] return $ (path, length contents) : concat rest
続いてモナド変換子(WriterT)を使う。WriterT [(FilePath, Int)] IO () という型は一見威圧されて目を背けたくなるところだが、単にIO箱を更にWriterT箱に包みつつそっちの箱に記録を取っていると考えればいいだけ。
元のcountEntriesTradと新しいcountEntriesを見比べてみるとliftIOっていうIOの箱の梱包を解いて中身を取り出す関数が増えているくらいだというのがわかる。
module CountEntriesT (listDirectory, countEntries) where import CountEntries (listDirectory) import System.Directory (doesDirectoryExist) import System.FilePath ((</>)) import Control.Monad (forM_, when) import Control.Monad.Trans (liftIO) import Control.Monad.Writer (WriterT, tell) countEntries ::FilePath -> WriterT [(FilePath, Int)] IO () countEntries path = do contents <- liftIO . listDirectory $ path tell [(path, length contents)] forM_ contents $ \name -> do let newName = path </> name isDir <- liftIO . doesDirectoryExist $ newName when isDir $ countEntries newName
countEntriesでwhenを使っていたのでcountEntriesTradでも使えるかなと思ったんだけど、型が合わなくて無理だった。ソース見ると
when :: (Monad m) => Bool -> m () -> m () when p s = if p then s else return () -- | The reverse of 'when'. unless :: (Monad m) => Bool -> m () -> m () unless p s = if p then return () else s
と定義されているのでIO ()型の時しか使えないのね。
おまけ
IO剥がすのになんでliftIOって言うんだろ?liftっていうのが自分の直感的なイメージに反するのだけど。