具体例から学ぶモナド再入門
Haskellで使うモナドはほんの数種類しかありません。それも3, 4種類に分けられ、同じ種類のモナドは性質も使い方も似ているため、モナドを学ぶならどうせ数種類しかないこれらの具体例を理解するのが近道です。
実際、ライブラリで定義されているモナドや自分自身で作るモナドは大体State
モナドか何かのnewtypeですし、GeneralizedNewtyoeDeriving
拡張のおかげでモナドのインスタンスを自力で書く機会もほとんどありません(本当か?)。
そこで、一般的な数種類のモナドの紹介をして、それにモナド再入門というタイトルをつけることにしました。
この記事を読むのに必要な知識
- Haskellの基本的な文法
- モナドの型クラス宣言を見たことがある(
return
と>>=
の存在を知っている)
モナドの基礎
モナドを扱う上で重要な概念は、アクションと実行の2つです。
- アクション
- モナドの型を持つ値のこと。
IO ()
やIO String
やList Int
など - 引数を与えるとアクションになる関数もたくさんある
-
putStr :: String -> IO ()
は、引数を与えればputStr "Hello" :: IO ()
というアクションになる
-
- モナドの型を持つ値のこと。
- 実行
- モナドを外すこと。モナドは大抵ただのnewtypeなので、ベースになっている型とモナドを行き来させることができる
- 通常
run*
という名前を持つ
4種類のモナド
モナドを大きく4つに分類しました。この分類は一般的なものではなく私が考えたものですので、この分類から漏れているモナドもありますし、他の人は違うように分類するかもしれません。
- List系
- 値を内包するモナド
- 他の言語にも登場するため、初心者にも馴染み深い
- State系
- 状態を持つモナド
- 分かりやすい鬼門
- Cont系
- 継続モナド
- これを学ぶのはListやStateの後で良い
- Free系
- 自由モナド
- 謎に流行ったことがある
Listモナド
任意の数の値を内包するモナドです。
亜種:
- Maybe
- Identity
- Either
MaybeとIdentityは、Listの保持する値の数を制限したものです。
EitherはMaybeの一般化ですね。
代表的なアクション:
- Nothing
- [], repeat, ..
- Left
特徴
初めて学ぶモナドはIO
よりもこちらではないでしょうか?
この類のモナドは下のような特徴を持ちます。
- コンストラクタが複数あるので、
run*
ができない(Identity除く) - モナドトランスフォーマー(MaybeT, ListTなど)の使用があまり一般的ではない
- Haskellを知らない人でも
>>=
の挙動に馴染みがある。JavaではflatMap
という名前でStreamやOptionalが持っている
Identityを除いて、これらモナドはしばしば例外処理に使われます。すなわち、例外処理の観点では、アクションが例外を投げることに相当します。
Identityは何もしないモナドで、モナドトランスフォーマーの文脈でよく使われます。
使用例
flatMap
っぽい使い方の例。
>>> print $ do
>>> a <- [1, 2, 3]
>>> b <- [4, 5, 6]
>>> return $ a * b
[4,5,6,8,10,12,12,15,18]
Stateモナド
状態を持つモナドです。
亜種:
- IO
- Reader
- Writer
->
- Kleisli
Stateは情報を内包するモナドで、アクションによって情報を読み書きすることができます。IO
は状態として「世界」を持たせたStateモナドで、これはアクションによって「世界」を更新しています。
Readerは読み込み限定、Writerは追記限定のStateモナドという認識でいいと思います。
関数->
も実はモナドであり、引数を状態のように引き回せます。Kleisliは初めて聞くかもしれませんが、この子は->
の親戚みたいなものです。->
がHask圏の射を表すのに対して、Kleisliはクライスリ圏の射を表します。
代表的なアクション:
-
get
,put
,modify
-
putStrLn s
,getLine
, ... -
ask
,tell
-
(*2)
,(+3)
, ...
特徴
いずれも関数をnewtypeしたものです。
m
は任意のモナドを表す型変数。Identityは何もしないモナドなので、このm
にIdentityを入れると下のm
のないバージョンを得られます。
- StateT:
s -> m (a, s)
- State:
s -> (a, s)
- State:
- ReaderT:
r -> m a
- Reader:
r -> a
- Reader:
- WriterT:
w -> m (a, w)
- Writer:
w -> (a, w)
- Writer:
-
->
:a -> b
- Kleisli:
a -> m b
StateTとWriterTは同型(直観的には同じ型、もう少し正確にすると情報を落とさずに相互に変換できる型)ですが異なるアクションを持ちます。WriterTのメソッドtell
は状態に対する追記をすることしかできません。
ReaderTとKleisliは同型で、Readerと->
も同型。これらはモナドとしては非常に似た振る舞いをします。
ところで、関数をnewtypeしたものということは、これらのモナドは小粋な名前のついただけの関数ということです。
関数のモナド的振る舞いというのはどういうものでしょうか?
例えば、引数にHandler
を受け取るような関数があります。
hPutChar :: Handle -> Char -> IO ()
hPutStr :: Handle -> String -> IO ()
hPutStrLn :: Handle -> String -> IO ()
型をよく見ると、出力する文字や文字列を先に与えれば、どれもHandle -> IO ()
の形にできます。
ReaderTはr -> m a
のnewtypeなので、この型にはReaderTをかぶせたりrunReaderT
で元の型に戻したりできます。
では実際にReaderTをかぶせるとどうなるでしょうか。mtl
のモジュールを使っています。
import Control.Monad.Reader
type Foo = ReaderT Handle IO
putCharM :: Char -> Foo ()
putCharM = ReaderT . flip hPutChar
-- (2つは省略)
見やすさのために、型シノニムでReaderT Handle IO
にFoo
と名付けました。
関数Handle -> IO ()
がアクションFoo ()
になり、引数からはHandle
が消えました。もちろんrunReaderT
で元の型に戻ります。
do文を使って、これらのアクションを繋げてみましょう。
doSomething :: Foo ()
doSomething = do
putCharM "H"
putStrM "ello, "
putStrLnM "monad!"
まるでHandleのない標準出入力用の関数のような使い心地になりました。
ところで、このdoSomething
もrunReaderT
を使えば元の型に戻ります。
doSomething' :: Handle -> IO ()
doSomething' = runReaderT doSomething
Handleを渡してあげればdoSomethingはIO ()
という見慣れたアクションになります。ここで渡したHandleは、ReaderTの下敷きになったhPutChar
などに渡されます。
import System.IO
main :: IO ()
main = do
handle <- openFile "output.txt" WriteMode
runReaderT doSomething handle
hClose handle
Contモナド
継続モナドです。
代表的なアクション:
- Cont
- callCC
- reset, shift
特徴
ListやStateに比べると、少し難解なモナドです。
ContTは(a -> m r) -> m r
という型をモナドのインスタンスにしたもので、これも関数型をnewtypeしたものですね。
(a -> m r) -> m r
という型は、リソース管理を要するwith*
関数に多く見られます。代表的なものはwithFile :: FilePath -> IOMode -> (Handle -> IO r) -> IO r
でしょう。
この一見奇妙な型は、リソースの獲得/解放をワンセットにしようと思うと生まれます。例えば、使用後は間違いなく閉じなければならないMyHandle
があったとしたら、下のような関数を提供したくなります。
import Control.Exception (finally)
withMyHandle :: (MyHandle -> IO r) -> IO r
withMyHandle processHandle = do
handle <- openMyhandle
processHandle handle
`finally`
closeMyHandle handle
この(MyHandle -> IO r) -> IO r
という型が、まさにContTモナドであるところの(a -> m r) -> m r
です。
ファイルハンドルを間違いなく閉じてくれるwithFile
を使って、ファイルをコピーするアクションを書いてみましょう。
withFile
を使えば、たとえどこで例外が発生してもファイルハンドルの閉じ忘れが起こりません。そのありがたみは、特に複数のファイルハンドラを同時に開く時に強く感じられます。
import System.IO
copyFile :: ContT () IO ()
copyFile = do
hSrc <- ContT $ withFile "srcFile.txt" ReadMode
hDst <- ContT $ withFile "dstFile.txt" WriteMode
src <- liftIO $ hGetContents hSrc
liftIO $ hPutStr hDst src
finallyがネストしているのに、インデントは平らなままです。
もちろん、runContT
すれば元の継続渡しスタイルに戻ります。return
で継続渡しスタイルも解除してやれば、綺麗さっぱりIO ()
が現れます。
copyFile' :: (() -> IO ()) -> IO ()
copyFile' = runContT copyFIle
copyFile'' :: IO ()
copyFile'' = copyFile' return
詳しくは、継続モナドについてという記事を投稿しているので、そちらを読んでいただけると幸いです。
Freeモナド
モナドを作るための枠組みです。
亜種:
- Operational
特徴
モナドを作るための枠組みであり、特徴はベースとなる構造によって与えられます。
発展的なものなので、詳しい解説はせず、紹介だけさらっと行います。
Freeは任意の関手からモナドを生み出すことができます。そして、任意の代数的データ型から関手を生み出せるCoyonedaとFreeを組み合わせ、どんな型からでもモナドを作り出せるようにしたものがOperationalです。
モナドのアクションとは、モナドの型を持つ値のことであると説明しました。つまり、モナドのコンストラクタは全てアクションを生む何かだということです。
そこで、コンストラクタでアクションを宣言することを考えます。
{-# LANGUAGE GADTs #-}
data ConsoleInst a where
GetContents :: ConsoleInst String
PutStr :: String -> ConsoleInst ()
Operationalモナドを使って、これをモナドにします。また、上で宣言したコンストラクタを実際にそのモナドのアクションにします。
実装にはmonad-skeletonを採用しました。
import Control.Monad.Skeleton
type Console = Skeleton ConsoleInst
getContents' :: Console String
getContents' = bone GetContents
putStr' :: String -> Console ()
putStr' = bone . PutStr
次に、モナドの実行を作成します。ここでどのアクションが何をするのかを定義します。
ここでは、GetContents
でgetContents
を、PutStr
でputStr
を実行するようにしました。
見慣れないコードに面食らうかもしれませんが、実際はただのボイラープレートです。
runConsoleIO :: Console a -> IO a
runConsoleIO c = case debone c of
Return a -> return a
GetContents :>>= k -> getContents >>= runConsoleIO . k
PutStr s :>>= k -> putStr s >>= runConsoleIO . k
もう一つ、今度はピュアなバージョンの実行を作成してみましょう。
unConsolePure :: Monad m => Console a -> ReaderT String (WriterT String m) a
unConsolePure c = case debone c of
Return a -> return a
GetContents :>>= k -> ask >>= unConsolePure . k
PutStr s :>>= k -> lift (tell s) >>= unConsolePure . k
runConsolePure :: Monad m => Console a -> String -> m (a, String)
runConsolePure = fmap runWriterT . runReaderT . unConsolePure
同じモナドを実行するのに、異なる複数の結果を得ることができています。インターフェースのようなある種の抽象化ができているということですね。
このモナドの実行時に各アクションの処理を定義する性質のおかげで、普通は悩みの種であるモックの作成も簡単です。
まとめ
ListとStateさえ学べば、Haskellを普通に使うには困らないと思います。
Contをさらっと使えればスマートに、FreeやOperationalをゴリゴリ使えれば変態になれます。