38
29

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Haskell リストモナド 超入門

Last updated at Posted at 2014-12-18

Haskellではモナドと呼ばれる部品を組み合わせてプログラムを作ります。IOモナドを取っ掛かりにリストをモナドとして扱いながら、モナドに共通する性質を探ります。モナドについての一般論へ進む前の準備を目的としているため、IOとリスト以外のモナドや圏論には言及しません。

シリーズの記事です。

  1. Haskell 超入門
  2. Haskell 代数的データ型 超入門
  3. Haskell アクション 超入門
  4. Haskell ラムダ 超入門
  5. Haskell アクションとラムダ 超入門
  6. Haskell IOモナド 超入門
  7. Haskell リストモナド 超入門 ← この記事
  8. Haskell Maybeモナド 超入門
  9. Haskell 状態系モナド 超入門
  10. Haskell モナド変換子 超入門
  11. Haskell 例外処理 超入門
  12. Haskell 構文解析 超入門
  13. 【予定】Haskell 継続モナド 超入門
  14. 【予定】Haskell 型クラス 超入門
  15. 【予定】Haskell モナドとゆかいな仲間たち
  16. 【予定】Haskell Freeモナド 超入門
  17. 【予定】Haskell Operationalモナド 超入門
  18. 【予定】Haskell Effモナド 超入門
  19. 【予定】Haskell アロー 超入門

練習の解答例は別記事に掲載します。

F#に応用した記事があります。

モナド

bind(>>=)とreturnで操作できる対象をモナドと呼びます。

※ bindとreturnが使えれば何でも良いわけではなく、モナド則というルールがあります。今回の範囲を超えるため詳細は省略しますが、興味があれば次の記事を参照してください。

IOモナド

IOモナドはモナドの一種で、次のような性質を持っています。

iomonad.png

  1. IOモナドは中に値を持っています。
  2. IOモナドと関数をbindでつなぐと、それらを含んだIOモナドが作れます。
  3. 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と同じです。

listmonad.png

異なる型

printはIOモナドを返すため、リストモナドとはbindできません。

NG
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 IntIOの部分を型変数化する場合、型変数に対してそれがモナドであることを指定する必要があります。これを型クラス制約と呼びます。

inc :: Monad m => Int -> m Int
inc x = return $ x + 1

main = do
    print $   inc =<< [1]
    print =<< inc =<< return 1
実行結果
[2]
2

Monad m =>の部分が型クラス制約で、mMonadであるということを示しています。次に示す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の型から仕様を推定して、コードで検証してください。

Control.Monadより

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が同種のモナドしか連結できないことに由来しています。

NG
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つの要素しか入っていないリストを見ていたため、<-は単なる値の取り出しだと見なせました。リストは複数の要素を含めるという特徴があるため、<-もそれに合わせて特有の動きをしています。

ループ

先ほどの例は慣れるまでは動きがイメージしにくいかもしれませんが、他の言語でのループに相当します。

ES2015
console.log(function () {
    let ret = [];
    for (let a of [1, 2, 3]) {  // 複数の値でのループ
        ret.push(a * 2);        // 繰り返される
    }
    return ret;
}());
実行結果(io.js)
[ 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]

これも先ほどと同じで、それぞれ<-から先の行がループしていると捉えれば、多重ループ構造が見えてくるのではないでしょうか。

ES2015
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  -- 処理されない
実行結果
[]

ソースが空ならループの中には入らないことで解釈できます。

ES2015
console.log(function () {
    let ret = [];
    for (let a of [1, 2, 3]) {
        for (let b of []) {     // ソースが空
            ret.push(a * b);    // 処理されない
        }
    }
    return ret;
}());
実行結果(io.js)
[]

ポリモーフィズム

モナドはdo<-などの同じ枠組みを使いながら、種類によって特有の動きがあります。これはオブジェクト指向でのポリモーフィズム(多態)に近い考え方です。

「モナド特有の動きをその都度覚えないといけない」と捉えるとうんざりしますが、仕様はモナドが含むデータの性質から決められています。そのことを意識してイメージすれば、それほど外すことはないはずです。

リストには複数の要素を含むことがあるため、取り出し動作も1つずつ行うというような感じです。

練習

【問3】次のコードをjoinforMで書き替えてください。

main = do
    print $ do
        x <- [1..3]
        y <- "abc"
        return (x, y)

解答例

【問4】リストモナドを扱うbindreturn'を実装してください。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で実装したbindreturn'に対応させてテストしてください。

解答例

まとめ

  • bind(>>=)とreturnで操作できる対象をモナドと呼びます。
  • リストはモナドの一種です。
  • IOモナドは中に値を生成する関数がありますが、リストは値が直接入っています。
  • 副作用を扱うのはIOモナド特有で、モナド共通の特徴ではありません。
  • doブロックは共通の見た目で記述できますが、モナドによって動きが異なります。
  • リスト内包表記はdoの糖衣構文です。

参考

モナドについては、次のツイートによくまとめられています。

これが理解できれば最初の壁は超えられたと思います。

38
29
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
38
29

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?