Haskellではモナドと呼ばれる部品を組み合わせてプログラムを作ります。IOモナドを取っ掛かりにリストをモナドとして扱いながら、モナドに共通する性質を探ります。モナドについての一般論へ進む前の準備を目的としているため、IOとリスト以外のモナドや圏論には言及しません。
シリーズの記事です。
- Haskell 超入門
- Haskell 代数的データ型 超入門
- Haskell アクション 超入門
- Haskell ラムダ 超入門
- Haskell アクションとラムダ 超入門
- Haskell IOモナド 超入門
- Haskell リストモナド 超入門 ← この記事
- Haskell Maybeモナド 超入門
- Haskell 状態系モナド 超入門
- Haskell モナド変換子 超入門
- Haskell 例外処理 超入門
- Haskell 構文解析 超入門
- 【予定】Haskell 継続モナド 超入門
- 【予定】Haskell 型クラス 超入門
- 【予定】Haskell モナドとゆかいな仲間たち
- 【予定】Haskell Freeモナド 超入門
- 【予定】Haskell Operationalモナド 超入門
- 【予定】Haskell Effモナド 超入門
- 【予定】Haskell アロー 超入門
練習の解答例は別記事に掲載します。
F#に応用した記事があります。
モナド
bind(>>=
)とreturn
で操作できる対象をモナドと呼びます。
※ bindとreturn
が使えれば何でも良いわけではなく、モナド則というルールがあります。今回の範囲を超えるため詳細は省略しますが、興味があれば次の記事を参照してください。
- @7shi: モナド則がちょっと分かった? 2015.3.9(改訂)
IOモナド
IOモナドはモナドの一種で、次のような性質を持っています。
- IOモナドは中に値を持っています。
- IOモナドと関数をbindでつなぐと、それらを含んだIOモナドが作れます。
-
return
で指定した値を入れたIOモナドが作れます。
これらはモナド共通の性質を反映してはいますが、他のモナドでは扱いが異なる部分もあります。IOモナドは内部に隠された関数から値を生成していますが、モナドの内部構造については規定がないため、必ずしも関数から値を生成しないといけないわけではありません。
IO以外のモナドとして、手始めにリストを取り上げます。
リストモナド
リストはモナドの一種です。モナドとしての側面を強調するときはリストモナドと呼ぶこともあります。
※ 今まで使って来た[1, 2, 3]
のようなリストを指しています。
return
return
で値を入れたリストが作れます。return
で異なる型を共通して作れるという特徴は、オブジェクト指向でのファクトリメソッドに近い考え方です。
return
だけでは何のモナドか分からないため、型注釈でリストモナドであることを指定します。型の書き方は2種類あります。[]
はリスト型を表しており、[] Int
の書式はIO Int
と同じように解釈します。
main = do
print [1]
print (return 1 :: [Int])
print (return 1 :: [] Int)
[1]
[1]
[1]
リストはprint
で中身ごと表示できるため、中に値が入っているのは直感的に分かります。IOモナドのように関数経由で値を生成しているわけではなく、値を直接持っています。
型推論
print
と結合するにはIOモナドである必要があるため、return
に型注釈を付けなければ、型推論によって自動的にIOモナドとして扱われます。
main = do
print =<< return 1
1
副作用
IOモナドは副作用を扱います。副作用がIOモナドの外に漏れないように、一度IOモナドに入れた値は原則取り出すことができません。
※ unsafePerformIO
は基本的に使ってはいけない関数です。
リストに対する処理は副作用がないため、リストからの値の取り出しは自由に行えます。値を見るだけなら取り出さなくてもリストのままで確認できます。
main = do
let a = return 1 :: IO Int
b = return 1 :: [] Int
print =<< a
print $ b !! 0
print b
1
1
[1]
副作用に縛られているのはIOモナドの特徴で、モナドに共通する特徴ではありません。言い換えると、副作用をモナドの枠組みで取り扱うために作られたのがIOモナドです。
練習
【問1】[
と]
は使わないで、return
を使って[1, 2, 3]
を作ってください。
⇒ 解答例
bind
bindでモナドを結合するとき、結合先の関数は同種のモナドを返す必要があります。
値を受け取ってリストを返す関数であれば、リストとbindできます。
main = do
print $ [7] >>= replicate 3
print $ "7" >>= replicate 3
[7,7,7]
"777"
[7]
はリストで、[7] >>= replicate 3
の結果もリストです。リストを関数とbindすればリストになるという構図は、IOモナドのbindと同じです。
異なる型
print
はIOモナドを返すため、リストモナドとはbindできません。
main = do
print =<< [1]
Couldn't match type `[]' with `IO'
Expected type: IO a0
Actual type: [a0]
(略)
リストは中身ごと表示できるため、わざわざ値を取り出して表示する必要はありません。
多相
bind先の関数に型注釈を書かないでreturn
を使えば、異なる種類のモナドを受け付ける関数ができます。このように異なる型をまとめて扱うことを多相と呼びます。
inc x = return $ x + 1
main = do
print $ inc =<< [1]
print =<< inc =<< return 1
[2]
2
型変数
多相で型注釈を書くときは、小文字で始まる適当な名前を付けます。これを型変数と呼びます。型名は大文字で始まると決まっているため、小文字で始まれば自動的に型変数となります。型変数の文字数に決まりはありませんが、一文字で書くことが多いです。
次のrep
の型注釈にあるa
が型変数です。
rep :: Int -> a -> [a]
rep 0 _ = []
rep n x = x : rep (n - 1) x
main = do
print $ rep 3 7
print $ rep 5 'a'
[7,7,7]
"aaaaa"
型クラス制約
IO Int
のIO
の部分を型変数化する場合、型変数に対してそれがモナドであることを指定する必要があります。これを型クラス制約と呼びます。
inc :: Monad m => Int -> m Int
inc x = return $ x + 1
main = do
print $ inc =<< [1]
print =<< inc =<< return 1
[2]
2
Monad m =>
の部分が型クラス制約で、m
がMonad
であるということを示しています。次に示すIO
や[]
を型変数で表すために付け加えていると解釈すれば良いでしょう。
a :: IO Int
b :: [] Int
c :: Monad m => m Int
Applicativeスタイル
Applicativeスタイルでは関数に渡されるのはモナドではないため、型クラス制約は意識する必要がありません。
import Control.Applicative
inc :: Int -> Int
inc = (+ 1)
main = do
print $ inc <$> [1]
print =<< inc <$> return 1
[2]
2
練習
【問2】次に示す関数join
の型から仕様を推定して、コードで検証してください。
join :: Monad m => m (m a) -> m a
⇒ 解答例
do
IOモナドと同じようにリストでもdo
が使えます。最後にreturn
でモナドを返すのも同様です。
test x = do
a <- [x]
return $ a + 1
main = do
print $ test 1
[2]
do
の実体がbindによる連結なのも同様です。
test x =
[x] >>= \a ->
return $ a + 1
main = do
print $ test 1
[2]
このようにIOモナドもリストも同じようにdo
やbindなどの枠組みで扱えることが、モナドとしての共通性です。
異なる型
1つのdo
の中では同じ種類のモナドしか扱えません。do
はbindの糖衣構文で、bindが同種のモナドしか連結できないことに由来しています。
main = do
a <- print "a"
b <- [1]
return ()
Couldn't match type `[]' with `IO'
Expected type: IO t0
Actual type: [t0]
ネストさせたdo
では別種のモナドが扱えます。
main = do
print $ do
a <- [1]
return $ a + 1
print $
[1] >>= \a ->
return $ a + 1
[2]
[2]
第1段階のdo
はIOモナド、第2段階のdo
はリストモナドとなっています。
複数の要素
リスト内の要素が複数あった場合の挙動を見てみます。
main = do
print $ do
a <- [1, 2, 3] -- 複数の値へのbind
return $ a * 2 -- 繰り返される
[2,4,6]
<-
から先の行が繰り返されて、結果が結合されてリストになります。
今までは1つの要素しか入っていないリストを見ていたため、<-
は単なる値の取り出しだと見なせました。リストは複数の要素を含めるという特徴があるため、<-
もそれに合わせて特有の動きをしています。
ループ
先ほどの例は慣れるまでは動きがイメージしにくいかもしれませんが、他の言語でのループに相当します。
console.log(function () {
let ret = [];
for (let a of [1, 2, 3]) { // 複数の値でのループ
ret.push(a * 2); // 繰り返される
}
return ret;
}());
[ 2, 4, 6 ]
※ 意図して手続的に書いています。ジェネレータに慣れている方はジェネレータでリストモナドを模倣してみたを参照してください。
forM
でループに書き替えてみます。
import Control.Monad
main = do
print $
forM [1, 2, 3] $ \x ->
return $ x * 2 :: [] Int
[[2,4,6]]
forM
はリストをモナドに包んで返します。この場合はリストモナドで包んでいるため、リストが二重になっています。
ネストしたモナドをjoin
で統合します。join
により型推論が効くためreturn
の型注釈は省略できます。
import Control.Monad
main = do
print $ join $ -- joinの追加
forM [1, 2, 3] $ \x ->
return $ x * 2 -- 型注釈の省略
[2,4,6]
多重ループ
do
の中で複数のリストから値を取り出せば多重ループとなります。
main = do
print $ do
a <- [1, 2, 3]
b <- [4, 5, 6] -- 追加
return $ a * b -- 変更
[4,5,6,8,10,12,12,15,18]
これも先ほどと同じで、それぞれ<-
から先の行がループしていると捉えれば、多重ループ構造が見えてくるのではないでしょうか。
console.log(function () {
let ret = [];
for (let a of [1, 2, 3]) {
for (let b of [4, 5, 6]) { // 追加
ret.push(a * b); // 変更
}
}
return ret;
}());
[ 4, 5, 6, 8, 10, 12, 12, 15, 18 ]
空のリスト
空のリストを与えると、後続の処理が行われません。
main = do
print $ do
a <- [1, 2, 3]
b <- [] -- 空のリスト
return $ a * b -- 処理されない
[]
ソースが空ならループの中には入らないことで解釈できます。
console.log(function () {
let ret = [];
for (let a of [1, 2, 3]) {
for (let b of []) { // ソースが空
ret.push(a * b); // 処理されない
}
}
return ret;
}());
[]
ポリモーフィズム
モナドはdo
や<-
などの同じ枠組みを使いながら、種類によって特有の動きがあります。これはオブジェクト指向でのポリモーフィズム(多態)に近い考え方です。
「モナド特有の動きをその都度覚えないといけない」と捉えるとうんざりしますが、仕様はモナドが含むデータの性質から決められています。そのことを意識してイメージすれば、それほど外すことはないはずです。
リストには複数の要素を含むことがあるため、取り出し動作も1つずつ行うというような感じです。
練習
【問3】次のコードをjoin
とforM
で書き替えてください。
main = do
print $ do
x <- [1..3]
y <- "abc"
return (x, y)
⇒ 解答例
【問4】リストモナドを扱うbind
とreturn'
を実装してください。bind
にはfoldr
を使ってください。
具体的には次のコードが動くようにしてください。
main = do
print $ [1..3] `bind` \x -> "abc" `bind` \y -> return' (x, y)
[(1,'a'),(1,'b'),(1,'c'),(2,'a'),(2,'b'),(2,'c'),(3,'a'),(3,'b'),(3,'c')]
⇒ 解答例
リスト内包表記
リストに特化したdo
の糖衣構文がリスト内包表記です。表記上の違いだけで、リストのモナド的な側面に既に触れていたわけです。
do
とリスト内包表記とを対比させます。<-
の役割がリスト内包表記とdo
で同じなのを確認してください。
main = do
print $ do
x <- [1..5]
return $ x * 2
print [x * 2 | x <- [1..5]]
print $ do
x <- [1..3]
y <- "abc"
return (x, y)
print [(x, y) | x <- [1..3], y <- "abc"]
[2,4,6,8,10]
[2,4,6,8,10]
[(1,'a'),(1,'b'),(1,'c'),(2,'a'),(2,'b'),(2,'c'),(3,'a'),(3,'b'),(3,'c')]
[(1,'a'),(1,'b'),(1,'c'),(2,'a'),(2,'b'),(2,'c'),(3,'a'),(3,'b'),(3,'c')]
練習
【問5】次のリスト内包表記をdo
で書き換えてください。
main = do
print [(x, y) | x <- [1..5], y <- [1..5], x + y == 6]
⇒ 解答例
【問6】問5のコードを問4で実装したbind
とreturn'
に対応させてテストしてください。
⇒ 解答例
まとめ
- bind(
>>=
)とreturn
で操作できる対象をモナドと呼びます。 - リストはモナドの一種です。
- IOモナドは中に値を生成する関数がありますが、リストは値が直接入っています。
- 副作用を扱うのはIOモナド特有で、モナド共通の特徴ではありません。
-
do
ブロックは共通の見た目で記述できますが、モナドによって動きが異なります。 - リスト内包表記は
do
の糖衣構文です。
参考
モナドについては、次のツイートによくまとめられています。
モナド ・手続きの性質を持つ圏論(数学の一分野)由来の概念 ・モナドによって純粋関数型でも手続きプログラミングが可能になる ・多態性を持つため組み込みの手続きより強力
— ちゅーん (@its_out_of_tune) 2015, 2月 23
これが理解できれば最初の壁は超えられたと思います。