LoginSignup
13
11

More than 5 years have passed since last update.

いろんなMonadをdo構文で

Last updated at Posted at 2016-12-11

いろんなMonadをdo構文で

環境

準備

ghciのコマンド

簡単にさらっと。

  • 対話的インタプリタ ghciを使用するときは、stack exec ghciとすれば、起動します。
    • .hsファイルをコンパイルする時は:l file.hs or :load file.hsでOKです。
    • モジュールのロードは:m +Moduleで追加、:m -Moduleで除去できます。

Note) :reloadで最後にコンパイルしたファイルをコンパイルできます。引数にfile.hsのように、ファイル名を明示する必要がないことがメリットです。

Monadとは

Haskellのモナドは、型クラスの一種として、ただの計算に文脈を付与する方法と文脈を伴う計算同士の組み合わせ方法を一緒に与えておくことで、どの文脈を持った計算であっても統一された文法で扱える強力な仕組みです。(中略)言語内にHaskellの機能をまるまる継承したDSL(ドメイン記述言語)を組めるようなものです。[2, p.241]


ざっくりとしたイメージとしては、Monadは箱みたいなものです。

箱自体をプレゼントされても困ると思います。それは、箱自体は中身のないものだからです。
しかし、箱を特殊化することにより、

段ボール => 引っ越し
カゴ => 買い物
タンス => 衣装を入れる

みたいに、様々な用途に使えたりします。Monadが「強力な仕組み」というのはそういうことです。Haskellでは、右側の意味づけを'文脈'と言ったりします。

上記の例をMonadに戻してみると

Maybe monad => 失敗可能性
List monad => 非決定性計算
IO monad => 副作用

みたいな感じになります。とは言っても、言葉だけだと「わかったような、わからんような」みたいな状態になるので、以下では、本節で述べたことを頭の片隅に置きながら、コードを読むと良いと思います。

(Monadについてイメージをもっと掴みたい方は、http://qiita.com/kazatsuyu/items/d1c9b97d92af89c4cca0 を見ると幸せになれるはず!)

(>>=)

  • (>>=)の定義自体は
(>>=) :: Monad m => m a -> (a -> m b) -> m b

で、関数a -> mb>>=に食わせてやると、m b型の値が出力されます:

> Just 3 >>= \x -> Just (x*2)
Just 6

return

return :: Monad m => a -> m a

returnは値をMonadの箱に突っ込むだけの関数です。

do構文

よく見かける例

Main.hs
-- getLine :: IO String
-- putStrLn :: String -> IO ()
main :: IO ()
main = do
  x <- getLine
  putStrLn $ "Hello " ++ x

Main.hs
main :: IO ()
main = 
  getLine >>= \x -> putStrLn $ "Hello " ++ x

と同じです。do構文は(>>=)(バインド、[1, p.287])のsyntax sugarなのです。
do構文のいいところは、左側の値x :: Stringが、あたかもMonadという箱(getLine :: IO String)の中身を取り出したかのように見える点です。

モナドであれば、IO monadに限らずなんでも使えます([1, p.296]に記載されてる。Maybe monadのdo記法の例は[1, p.297]にある).

上記のJust 3 >>= \x -> Just (x*2)はdo構文を用いると

calc.hs
calc :: Maybe Int
calc = do
  x <- Just 3
  return (x*2)

とか、ghci上でワンライナーで書きたいときは、

> do { x <- Just 3; return (x*2) }

と書けば良いです。

Note) ghci上でどうしても複数行を書きたい場合は:{:}により、先頭と末尾を明示します。

Prelude> :{
Prelude| calc = do
Prelude|   x <- Just 3
Prelude|   return (x*2)
Prelude| :}
Prelude> calc
Just 6

ということで、Maybe, Either, リスト, Writer, Reader, State monadでdo構文を明示的に使った場合とそうでなく、バインド(>>=)そのものを使用した場合をずらっとまとめてみました。


do構文とバインド(>>=)を使った例

Maybe monad

  • Maybeの定義
-- Nothingが失敗したという文脈を持っている
data Maybe a = Nothing | Just a
  • (>>=)の定義
instance  Monad Maybe  where
    (Just x) >>= k      = k x
    Nothing  >>= _      = Nothing
  • do構文を用いて
calc.hs
-- calc :: Maybe Int
calc = do
  x <- Just 3
  y <- Just 5
  z <- Nothing
  -- return (x + y) -- Just 8
  return (x + y + z) -- Nothing
  • (>>=)を用いて
> let calc = Just 3 >>= \x -> Just 5 >>= \y -> return (x + y) in calc
8
> let calc = Just 3 >>= \x -> Just 5 >>= \y -> Nothing >>= \z -> return (x + y + z) in calc
Nothing

Note) Just 3 >>= \x -> Just 5 >>= \y -> return (x + y)は、

Just 3 >>= (\x -> (Just 5 >>= (\y -> return (x + y)) ) )

のように右側から左方向に見ていきます。以下の例でも同様。

Either monad

  • Eitherの定義
-- MaybeのNothingはエラー時の情報を持たなかったが、
-- Either型クラスはLeftにエラー情報を付加することができる。
data Either a b = Left a | Right b
  • (>>=)の定義
instance Monad (Either e) where
        Right m >>= k = k m
        Left e  >>= _ = Left e
  • do構文を用いて
calc.hs
calc :: Either String Int
calc = do
  x <- Right 5
  y <- Right 3
  z <- Left "NG"
  -- return (x + y) -- Right 8
  return (x + y + z)  -- Left "NG" 
  • (>>=)を用いて
> let calc = Right 5 >>= \x -> Right 3 >>= \y -> return (x + y) in calc
Right 8
> let calc = Right 5 >>= \x -> Right 3 >>= \y -> Left "NG" >>= \z -> return (x + y + z) in calc
Left "NG"

リスト monad

  • []の定義
-- 非決定的(優柔不断な)計算ができる
data [] a = [] | a : [a]
  • (>>=)の定義
instance Monad []  where
  -- xs >>= f = concat $ map f xs ともかける
  -- concat :: Foldable t => t [a] -> [a] Foldableをぺしゃんこに平坦化する
  -- concat [[2,3],[3,4],[4,5]] で[2,3,3,4,4,5]となる
  xs >>= f = [y | x <- xs, y <- f x]
  • do構文を用いて
calc.hs
-- calc :: [Int]
calc = do
  x <- [1..3]
  y <- [1..2]
  return (x+y) -- [2,3,3,4,4,5]
  • (>>=)を用いて
> let calc = [1..3] >>= \x -> [1..2] >>= \y -> return (x + y) in calc
[2,3,3,4,4,5]

IO monad

Note) https://wiki.haskell.org/IO_inside を参考にしました1

  • IOの定義
-- 副作用を扱える。というより、副作用が混じっている部分をIOモナドに閉じ込めて分離ができる。本節の注釈も参照
-- Haskellが純粋なのに、なぜ副作用を扱えるかのざっくりとした説明は[2, p.281]を見ると良い。
type IO a  =  RealWorld -> (a, RealWorld)

-- Here is the actual IO definition from the GHC sources:
-- "(# #)" strict tuple for optimization
newtype IO a = IO (State# RealWorld -> (# State# RealWorld, a #))
  • (>>=)の定義
(>>=) :: IO a -> (a -> IO b) -> IO b
(action1 >>= action2) world0 =
   let (a, world1) = action1 world0
       (b, world2) = action2 a world1
   in (b, world2)
  • do構文を用いて
calc.hs
-- getLine :: IO String
-- putStrLn :: String -> IO ()
calc :: IO ()
calc = do
  x <- getLine
  y <- getLine
  putStrLn $ "Hello " ++ x ++ " " ++ y -- $input1<CR>$input2<CR>Hello $input1 $input2 
  • (>>=)を用いて
> let calc = getLine >>= \x -> getLine >>= \y -> putStrLn $ "Hello " ++ x ++ " " ++ y in calc
hi -- input
good -- input
Hello hi good

Writer monad

  • Writerの定義
-- [1]ではこんな感じの定義になっている
-- wの部分が今までの履歴を残すという文脈に。
newtype Writer w a = Writer {runWriter :: (a, w)}

-- import Control.Monad.Writer
-- :info Writerで調べた結果
-- WriterTはモナド変換子(モナドの一種)。モナド変換子については、補足も参照。
-- Writerのさらに抽象化された型クラスWriterTが定義されている。
type Writer w = WriterT w Data.Functor.Identity.Identity :: * -> *
newtype WriterT w (m :: * -> *) a = WriterT {runWriterT :: m (a, w)}
  • (>>=)の定義
-- [1]ではこんな感じ
-- タプルの1番目の要素:fが作用する 2番目の要素:結合(Monoid型クラスのmappend関数が使用されている)
instance (Monoid w) => Monad (Writer w) where
(Writer (x, v)) >>= f = let Writer (y, v') = f x in Writer (y, v `mappend` v')

-- http://www.geocities.jp/m_hiroi/func/haskell30.html を参照
instance (Monoid w, Monad m) => Monad (WriterT w m) where
  m >>= k  = WriterT $ do (y, v) <- runWriterT m
                          (z, v') <- runWriterT (k y)
                          return (z, v `mappend` v')
  • do構文を用いて
calc.hs
import Control.Monad.Writer

calc :: Writer [String] Int
calc = do
  x <- writer (3, ["input : " ++ show 3])
  y <- writer (5, ["input : " ++ show 5])
  -- WriterT : モナド変換子(モナドの一種)
  -- runWriter calc とすれば、(8,["input : 3","input : 5"])
  return (x+y) -- WriterT (Identity (8,["input : 3","input : 5"]))

ただし、

-- writer関数
class (Monoid w, Monad m) => MonadWriter w m | m -> w where
    writer :: (a,w) -> m a
  • (>>=)を用いて
-- :m +Control.Monad.Writer
> let calc = writer (3, ["input : " ++ show 3]) >>= \x -> writer (5, ["input : " ++ show 5]) >>= \y -> return (x + y) :: Writer [String] Int in runWriter calc
(8,["input : 3","input : 5"])

Reader monad

ちょっとした解説

Reader monadは初見では分かりにくいのですが、1変数関数$f_i (i = [1,N) )$として、

-- 例として、N = 3
-- fmap ($ 4) [f1, f2, f3] は [7,20,2] となるので、7+20-2で25となる
> let func opr x = opr $ fmap ($ x) [f1, f2, f3] where {f1 = (+3); f2 = (*5); f3 = (div 8)} in func (\(x1:x2:x3:[]) -> x1+x2-x3) 4
25

というように、作用される値xを介さず、関数f1, f2, f3のみからfunc (\(x1:x2:x3:[]) -> x1+x2-x3) :: (Num t, Integral b) => b -> tみたいな1変数関数が作れるところが特徴です。関数合成f . gとは異なることに注意してください。

多分一番初めにdo構文の使用例を見るのが一番わかりやすいのではないかと。

  • Readerの定義

a. (->) a ba -> bのこと。

> :info (->)
data (->) t1 t2

b. Control.Monad.Readerを用いる場合

-- newtypeについては補足に記載しました。
newtype Reader env a = Reader {runReader :: env -> a}

-- import Control.Monad.Reader
-- :info Readerで調べた結果
-- Reader型クラスのさらに抽象化された型クラスReaderTが定義されている。
type Reader r = ReaderT r Data.Functor.Identity.Identity :: * -> *
-- ReaderT : モナド変換子(モナドの一種)
newtype ReaderT r (m :: k -> *) (a :: k) = ReaderT {runReaderT :: r -> m a}
  • (>>=)の定義

a. (->)(関数作用)そのものが実はMonadなのです。

-- GHC.Base
instance Monad ((->) env) where
    f >>= k = \e -> k (f e) e

b. Control.Monad.Readerを用いる場合

-- [3, p.269]
-- f : env -> a (where f x = f_1 x + f_2 x + .. + f_N x)というような関数を作れる
instance Monad (Reader env) where
  Reader f >>= g = Reader (\e -> RunReader (g (f e) e)

-- http://www.geocities.jp/m_hiroi/func/haskell30.html を参照
instance Monad m => Monad (ReaderT r m) where
  return x = ReaderT $ \_ -> return x
  m >>= k  = ReaderT $ \r -> do a <- runReaderT m r
                                runReaderT (k a) r
  • do構文を用いて

a. (->)をそのまま用いた場合

calc.hs
import Data.Char
readInt = read :: String -> Int

-- useless example:(, but this is easy? to grasp what it is:)
calc :: String -> Int
calc = do
  x <- (sum . map digitToInt) -- (->) String
  y <- readInt -- (->) String
  -- x :: Int, y :: Int
  return (x+y) -- calc "12" is 15 :: Int 
  -- (sum . map digitToInt) "12" + readInt "12"という計算をする。

b. Control.Monad.Readerを用いた場合

calc.hs
import Data.Char
import Control.Monad.Reader
readInt = read :: String -> Int

-- same as above!
calc :: Reader String Int
calc = do
  -- type String = [Char] なので、両者は同一です
  x <- reader (sum . map digitToInt) -- :: MonadReader [Char] m => m Int
  y <- reader readInt -- MonadReader String m => m Int
  -- x :: Int, y :: Int
  return (x+y) -- runReader calc "12" が15となる。
  --ReaderTは関数自体を包んでいるので、WriterTのようにそのままではshowできない。

ただし、

class Monad m => MonadReader r m | m -> r where
    reader :: (r -> a) -> m a
  • (>>=)を用いて

a. (->)をそのまま用いた場合

-- import Data.Char
> let calc = (sum . map digitToInt) >>= \x -> (read :: String -> Int) >>= \y -> return (x + y) in calc "12"
15

b. Control.Monad.Readerを用いた場合

-- import Data.Char
-- import Control.Monad.Reader
> let calc = reader (sum . map digitToInt) >>= \x -> reader (read :: String -> Int) >>= \y -> return (x + y) :: Reader String Int in runReader calc "12"

State monad

haskellは純粋なので、参照透過性(いつ、どの部分で実行しても、常に同じ結果を返す性質)が担保されます。つまり、変数を書き換えたりはできません。でも、「状態」が時間や状況によって変わってしまう場合も時にはあります(マルコフ連鎖とかはまさにそういう感じだよね)。
その時は過去の履歴に応じて、次の行動が決まるわけで、State monadを用いることで、必要となる過去の履歴という状態を保存しつつ、求めたい値も導出できる。どういうことかというと、Stateの定義から見ていくのが手っ取り早いです。

  • Stateの定義
-- Reader Monadと同じく関数を包んでいる形。
-- (a, s)の a : 求めるべき値 , s : 状態(現在の値と過去の履歴)
newtype State s a = State {runState :: s -> (a, s)} 

-- import Control.Monad.State
-- :info Stateで調べた結果
-- StateTはモナド変換子(モナドの一種)。モナド変換子については、補足も参照。
-- Stateのさらに抽象化された型クラスStateTが定義されている。
type State s = StateT s Data.Functor.Identity.Identity :: * -> *
newtype StateT s (m :: * -> *) a = StateT {runStateT :: s -> m (a, s)}
  • (>>=)の定義
-- [1]ではこんな感じ
-- タプルの1番目の要素:gが作用する 2番目の要素:prevの状態s1が保存されてる)
instance Monad (State s) where
  (State f) >>= g = State $ \s -> let (x, s1) = f s in runState (g x) s1

-- http://www.geocities.jp/m_hiroi/func/haskell30.html を参照
instance Monad m => Monad (StateT s m) where
  m >>= k  = StateT $ \s -> do (a, s') <- runStateT m s runStateT (k a) s'
  • do構文を用いて
calc.hs
import Control.Monad.State
-- [Int]は状態(この例だと、スタック)
-- Intは出力値
calc :: State [Int] Int
calc = do
  state $ \xs -> ((), 10:xs) -- push 10
  a <- state $ \(x:xs) -> (x, xs) -- a <- pop
  b <- state $ \(x:ys) -> (x, ys) -- b <- pop
  return (a+b) -- runState calc [5,4..1] として、(15,[4,3,2,1])を得る。

ただし、

class Monad m => MonadState s (m :: * -> *) | m -> s where
  state :: (s -> (a, s)) -> m a

pop関数、push関数を作ってしまうと、分かりやすい:

calc.hs
import Control.Monad.State

pop :: State [Int] Int
pop = state $ \(x:xs) -> (x, xs)

push :: Int -> State [Int] ()
push a = state $ \xs -> ((), a:xs)

calc :: State [Int] Int
calc = do
  push 10
  a <- pop
  b <- pop
  return (a+b) -- runState calc [5,4..1] として、(15,[4,3,2,1])を得る。
  • (>>=)を用いて
-- :m +Control.Monad.State
-- popとpushは事前に上記のように定義しておきます。
> let calc = push 10 >> pop >>= \a -> pop >>= \b -> return (a + b) in runState calc [5,4..1]

もちろん、Monadはほかにもいろいろありますが、とりあえず [1] などのHaskell本に記載されているMonadは網羅しました。


補足

newtype

データを包むnewtypeとMonadは相性が良いです(上記の例だと、Writer, Reader, Stateモナド)。newtypeの挙動について簡易的ですがまとめておきます。

> newtype Tup a = Tup { getTuple :: (a, a) } deriving (Show)
> let x = Tup (6,9) -- setするときは(a, a)として直接突っ込めば良い
> x
Tup {getTuple = (6,9)}
-- getTuple :: Tup a -> (a, a)
> getTuple x -- getするときは x :: Tup型を引数に取れば良い。
(6,9)

monad 変換子

https://github.com/Kinokkory/wiwinwlh-jp/wiki/モナド変換子
あたりがわかりやすかったです2

[2, p.261]にもわかりやすい記載があります。モナド変換子はあるモナドを別の新しいモナドに進化させることのできる(文脈を合成することができる)モナドです3。モナド変換子はWriterT, ReaderT, StateTとすでに出てきましたが、自明なモナドIdentityを定義することで、モナド変換子さえ用意すれば、今回扱うモナドのWriter, Reader, Stateが作れます。

参考文献

  1. すごいHaskellたのしく学ぼう!

  2. 関数プログラミング実践入門

  3. http://www.geocities.jp/m_hiroi/func/haskell30.html


  1. 3章の冒頭の注意書きにあるように、正確ではないけど、IO monadのとっかかりとしてはよい的なことが書いてあります。本節もその意向に沿います。 

  2. リンク先の冒頭部分:「前章でのモナドの説明は、罪のない嘘が少々交じっています」とある通り、本記事もそこらへんはちょっとごまかしています。いきなりモナド変換子に踏み込むと混乱するので、ここでは、実際にはGHCには、こう定義されているんだな、という認識で良いかと思います。モナド変換子の詳細については、本記事の範囲を超えるので省略します。 

  3. よくRPGで出てくる、武器合成みたいなもんです。 

13
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
11