Haskellではモナドと呼ばれる部品を組み合わせてプログラムを作ります。関数の評価に失敗したときにエラーを起こさずに処理する方法の初歩を説明します。Maybeモナドを手っ取り早く使うことを目的としているため、モナドの作り方や圏論には言及しません。
シリーズの記事です。
- 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
で操作できる対象をモナドと呼びます。(※) - 副作用を扱うのはIOモナド特有で、モナド共通の特徴ではありません。
-
do
ブロックは共通の見た目で記述できますが、モナドによって動きが異なります。
今までIOモナドとリストモナドを見て来ましたが、今回は失敗系と呼ばれるMaybeモナドを取り上げます。
※ bindとreturnが使えれば何でも良いわけではなく、モナド則というルールがあります。今回の範囲を超えるため詳細は省略しますが、興味があれば次の記事を参照してください。
- @7shi: モナド則がちょっと分かった? 2015.3.9(改訂)
失敗系モナド
不正な引数を渡した時などに関数でエラーが発生して評価が失敗することがあります。このようなときにエラーを出さないで処理できるモナドは失敗系モナドと呼ばれます。
今回取り上げる失敗系モナドは次の2つです。
- Maybeモナド
- リストモナド
リストモナドと比較しながら新出のMaybeモナドについて説明します。
Maybeモナド
Maybe a
「1個までしか値が持てないリスト」に相当するモナドです。個数が制限されている以外の挙動は基本的に同じです。
表記が異なるので比較します。
モナド | 0個 | 1個 | 2個 | 3個 | 4個以上 |
---|---|---|---|---|---|
リスト | [] |
[x] |
[x, y] |
[x, y, z] |
(略) |
Maybe | Nothing |
Just x |
なし | なし | なし |
0個は値がありませんが、これにより関数が評価に失敗して値を返せないことが表現できます。
Just
値が入ったMaybeモナドはreturn
でもJust
でも生成できます。
main = do
let a = return 1 :: Maybe Int
b = Just 1
print (a, b)
(Just 1,Just 1)
型注釈が不要なJust
の方が使いやすいです。この記事ではdo
ブロックの末尾でのみreturn
を使用します。
関数で失敗を返す
Haskell 超入門で取り上げた階乗の実装では、マイナスの引数はガードで弾いてエラーにしていました。
使い分け - 併用より再掲
fact 0 = 1
fact n | n > 0 = n * fact (n - 1)
main = do
print $ fact (-1) -- 実行時エラー
Main.hs:(1,1)-(2,33): Non-exhaustive patterns in function fact
エラーで処理が止まるのを防ぐため、評価に失敗したときはNothing
を返すようにします。型を合わせるため成功時には値をJust
で包む必要があります。
import Control.Applicative
fact 0 = Just 1
fact n | n > 0 = (n *) <$> fact (n - 1)
| otherwise = Nothing
main = do
print $ fact (-1)
print $ fact 5
Nothing
Just 120
値がないことで失敗を表現したため、エラーにならずに先に進めるようになりました。
練習
【問1】次の関数fib
でエラーを回避するためMaybeモナドで書き換えてください。
fib 0 = 0
fib 1 = 1
fib n | n > 1 = fib (n - 2) + fib (n - 1)
main = do
print $ fib (-1)
print $ fib 6
⇒ 解答例
bind
Maybeモナドの挙動はリストと基本的に同じです。
確認のため<-
でJust
とNothing
から値を取り出してみます。
main = do
print $ do
a <- Just 1 -- 値がある
return $ a * 2 -- 処理される
print $ do
a <- Just 1 -- 値がある
b <- Nothing -- 値がない
return $ a * b -- 処理されない
Just 2
Nothing
なぜ最後の行が処理されないのかを、リストに置き換えて考えてみます。Maybeモナドと同じ挙動なのを確認してください。
main = do
print $ do
a <- [1] -- 値がある
return $ a * 2 -- 処理される
print $ do
a <- [1] -- 値がある
b <- [] -- 値がない
return $ a * b -- 処理されない
[2]
[]
リストからの値の取り出しはループと見なせるため、ソースが空ならループの中には入りません。
console.log(function () {
var ret = [];
for (var a of [1]) { // 値がある
ret.push(a * 2); // 処理される
}
return ret;
}());
console.log(function () {
var ret = [];
for (var a of [1]) { // 値がある
for (var b of []) { // 値がない(ソースが空)
ret.push(a * b); // 処理されない
}
}
return ret;
}());
[ 2 ]
[]
取り出しの失敗
リストは複数の値を持てるためループで解釈するのが自然ですが、値が1個までしか持てないMaybeモナドをループで解釈するのは不自然です。
ループのことは考えずに、原則に立ち返って<-
では1つの値が取り出されるという目で見直します。
main = do
print $ do
a <- Just 1 -- 1を取り出す
return a -- モナドに入れて返す
Just 1
Nothing
から値を取り出そうとしても失敗します。
main = do
print $ do
a <- Just 1 -- 1を取り出す
b <- Nothing -- 取り出せない(失敗)
return $ a * b -- 処理されない
Nothing
取り出しに失敗した時点でdo
から抜けてNothing
を返すと解釈します。
※ ループで解釈しても結果は同じですが、Maybeモナドでは抜けると解釈した方が簡単だということです。
他の言語で実装すれば、値を取り出すタイミングで毎回チェックすることになります。
console.log(function () {
var a = [1].pop(); // 要素を取り出す
if (a == undefined) return []; // 取り出せたかチェック
var b = [].pop(); // 要素を取り出す(失敗)
if (b == undefined) return []; // ここで引っ掛かる
return [a * b]; // 処理されない
}());
[]
※ undefined
で失敗が表現されているのはNothing
に似ています。
パターンマッチ
<-
はMaybeモナドを扱うdo
の中でしか使えません。それ以外はパターンマッチでJust
から値を取り出します。
main = do -- IOモナドを扱うdo(対象外)
let a = Just 1
(Just a') = a -- パターンマッチ
print a'
1
Nothing
パターンマッチでNothing
から値を取り出そうとすると、コンパイルは通りますが実行時にエラーが発生します。
main = do
let a = Just 1
b = Nothing
(Just a') = a -- OK
(Just b') = b -- 実行時エラー
print [a', b']
Main.hs:5:9-21: Irrefutable pattern failed for pattern (Data.Maybe.Just b')
エラー対策
Maybeモナドに対してパターンマッチするときは、必ずJust
とNothing
の両方のパターンを記述するようにします。
test (Just x) = x -- 引数でパターンマッチ
test Nothing = 0 -- 忘れずに書く
main = do
print $ test $ Just 1
print $ test $ Nothing
1
0
練習
【問2】Maybeモナドを扱うbind
を実装してください。do
や>>=
は使わないでください。
具体的には次のコードが動くようにしてください。
main = do
print $ Just 1 `bind` \a -> Just $ a * 2
print $ Just 1 `bind` \a -> Nothing `bind` \b -> Just $ a * b
Just 2
Nothing
⇒ 解答例
失敗を含む処理
Maybeモナドを利用した失敗を含む処理をいくつか紹介します。
全てか無か
異常を検知した時点で処理を中断します。
import Control.Applicative
fact 0 = Just 1
fact n | n > 0 = (n *) <$> fact (n - 1)
| otherwise = Nothing
facts n = do
a <- fact $ n
b <- fact $ n - 1
c <- fact $ n - 2
return (a, b, c)
main = do
print $ facts 3
print $ facts 2
print $ facts 1 -- cで失敗
print $ facts 0 -- bで失敗
Just (6,2,1)
Just (2,1,1)
Nothing
Nothing
値の取り出しが1つでも失敗すればNothing
が返ります。全てか無かです。
mapMaybe
Data.Maybeモジュールの関数です。
mapMaybe :: (a -> Maybe b) -> [a] -> [b]
Maybeモナドを外してリストを返すmap
です。
-
Just
: 取り出した値をリストに追加します。 -
Nothing
: リストから除外します。
途中で抜けずに、成功した結果だけを見たいときに便利です。
map
と挙動を比較します。
import Control.Applicative
import Data.Maybe
fact 0 = Just 1
fact n | n > 0 = (n *) <$> fact (n - 1)
| otherwise = Nothing
facts n = ( map fact [n, n - 1, n - 2]
, mapMaybe fact [n, n - 1, n - 2]
)
main = do
print $ facts 3
print $ facts 2
print $ facts 1 -- cで失敗
print $ facts 0 -- bで失敗
([Just 6,Just 2,Just 1],[6,2,1])
([Just 2,Just 1,Just 1],[2,1,1])
([Just 1,Just 1,Nothing],[1,1])
([Just 1,Nothing,Nothing],[1])
mapMaybe
ではMaybeモナドが取り払われたリストが得られます。
※ このサンプルは次の練習で使います。
練習
【問3】mapMaybe
を再帰で再実装して、先ほどのサンプルで検証してください。
⇒ 解答例
【問4】問3の解答をfoldr
で書き換えてください。
⇒ 解答例
doからの脱出
do
の中にNothing
だけの行を書くと_ <- Nothing
と解釈されるため、<-
での脱出チェックが働きます。[]
も同様です。
※ <-
の有無に関わらずdo
の各行はbindで結ばれます。
C言語などでのreturn
が持つ働きに似ていますが、値を返すことはできません。
main = do
print $ do
Nothing -- 脱出
return 1 -- 処理されない
print $ do
[] -- 脱出
return 1 -- 処理されない
Nothing
[]
条件脱出
条件によってNothing
を返す行を書くことで、条件によってdo
から脱出できます。
文字列の先頭3文字が「大文字+小文字+数字」で構成されているかチェックする例です。
import Data.Char
import Control.Monad
getch s n
| n < length s = Just $ s !! n -- 指定した位置の文字を返す
| otherwise = Nothing -- 範囲外
test s = do
ch0 <- getch s 0 -- 文字列の範囲外なら脱出
ch1 <- getch s 1 -- 文字列の範囲外なら脱出
ch2 <- getch s 2 -- 文字列の範囲外なら脱出
unless (isUpper ch0) Nothing -- 大文字でなければ脱出
unless (isLower ch1) Nothing -- 小文字でなければ脱出
unless (isDigit ch2) Nothing -- 数字でなければ脱出
return s -- 最終的な返り値
main = do
print $ test "Aa0" -- OK
print $ test "A" -- 文字不足
print $ test "aa0" -- ch0で失敗
print $ test "AA0" -- ch1で失敗
print $ test "Aaa" -- ch2で失敗
Just "Aa0"
Nothing
Nothing
Nothing
Nothing
ここではunless
を使用しましたが、専用の関数があります。
guard
Control.Monadモジュールの関数です。
guard :: MonadPlus m => Bool -> m ()
引数がFalse
のときにdo
ブロックを抜けます。C言語のassert
に似ていますが、エラーは発生しません。
※ MonadPlus
は値を持たないことが可能なモナドを示します。MaybeモナドはNothing
、リストモナドは[]
があるため該当します。IOモナドなど内部の要素数が固定のモナドは該当しません。
先ほどの例を書き換えます。
import Data.Char
import Control.Monad
getch s n
| n < length s = Just $ s !! n
| otherwise = Nothing
test s = do
ch0 <- getch s 0
ch1 <- getch s 1
ch2 <- getch s 2
guard $ isUpper ch0 -- 使用箇所
guard $ isLower ch1 -- 使用箇所
guard $ isDigit ch2 -- 使用箇所
return s
main = do
print $ test "Aa0" -- OK
print $ test "A" -- 文字不足
print $ test "aa0" -- ch0で失敗
print $ test "AA0" -- ch1で失敗
print $ test "Aaa" -- ch2で失敗
Just "Aa0"
Nothing
Nothing
Nothing
Nothing
途中で抜けるという挙動が暗黙的に織り込まれていることを再確認してください。
練習
【問5】文字列s
が「x
文字の連続した数字+y
文字の連続した大文字」で構成されるか判定する関数numUpper x y s
を実装してください。成功した場合はJust s
、失敗した場合はNothing
を返してください。余計な文字は含まないものとします。
具体的には次のコードが動くようにしてください。
main = do
print $ numUpper 3 2 "123AB"
print $ numUpper 3 2 "123ABC"
print $ numUpper 3 2 "12ABC"
⇒ 解答例
Alternative
MonadPlus(値を持たないことが可能なモナド)だけで使えるApplicativeスタイルの一種です。評価に失敗したとき、別の評価を試せます。Alternative(オルタナティブ)は代替・二者択一という意味です。
※ この説明は色々と妥協しています。正確にはMonoidの知識が必要ですが、今回の範囲を超えるため詳細は省略します。興味がある方は次の記事を参照してください。
- @kazu_yamamoto: 【PDF】あなたの知らない Monoid の世界 2012.11.18
OR
Alternativeを説明する前に、条件式に使用する||
(OR)の挙動を確認します。
左 || 右
は左→右の順に評価されます。どちらか片方がTrue
であれば良いので、左がFalse
のときしか右は評価されません。
評価状況をトレースして確認します。
import Debug.Trace
isA ch = trace (show ch) $ ch == 'A'
main = do
traceIO $ show $ isA 'A' || isA 'B' -- 左のみ
traceIO "---"
traceIO $ show $ isA 'A' || isA 'A' -- 左のみ
traceIO "---"
traceIO $ show $ isA 'B' || isA 'A' -- 左 → 右
traceIO "---"
traceIO $ show $ isA 'B' || isA 'C' -- 左 → 右
'A'
True
---
'A'
True
---
'B'
'A'
True
---
'B'
'C'
False
<|>
Alternativeの演算子です。||
と同じ要領で、左がNothing
のときだけ右を評価します。
import Control.Applicative
main = do
print $ Just 1 <|> Nothing -- 左のみ
print $ Just 1 <|> Just 2 -- 左のみ
print $ Nothing <|> Just 2 -- 左 → 右
print (Nothing <|> Nothing :: Maybe Int) -- 左 → 右(要型注釈)
Just 1
Just 1
Just 2
Nothing
Nothing
だけではMaybeモナドの中の型が決められないため、最後の例では型注釈を加えています。
リスト
リストに対して<|>
を使うと択一ではなく列挙されます。常に左と右の両方が評価されます。
import Control.Applicative
main = do
print $ [1] <|> [] -- 左 → 右
print $ [1] <|> [2] -- 左 → 右
print $ [] <|> [2] -- 左 → 右
print ([] <|> [] :: [Int]) -- 左 → 右(要型注釈)
[1]
[1,2]
[2]
[]
Maybeモナドとの違いについては「Maybeモナドは値が複数持てないため、1つ目が確定した時点で処理を完了する」と解釈すれば良いでしょう。
do
do
とdo
を<|>
でつなぐこともできます。
2つに分岐した流れが合流する様子は、if
~then
~else
による条件分岐に似ています。
import Control.Applicative
import Control.Monad
check x = do
do -- 分岐点
guard $ x == 1 -- 分岐1
<|> do
guard $ x == 3 -- 分岐2
Just x -- 合流
main = do
forM_ [1..5] $ \x -> print $ check x
Just 1
Nothing
Just 3
Nothing
Nothing
練習
【問6】文字列s
の先頭3文字が「数字+大文字+小文字」または「大文字+小文字+小文字」で構成されるかを判定する関数check s
を実装してください。<|>
を使って、成功した場合はJust s
、失敗した場合はNothing
を返してください。後続の文字は判定に影響しないものとします。
具体的には次のコードが動くようにしてください。
main = do
print $ check "1"
print $ check "2Ab"
print $ check "Abc"
print $ check "Ab1"
print $ check "1AB"
⇒ 解答例
まとめ
Maybeモナドはリストの機能限定版のようなモナドですが、色々な応用があります。
-
Nothing
: 関数で値が返せないことを表現 -
<-
,guard
: 処理の途中で脱出 -
mapMaybe
: 成功と失敗が混ざったリストから、成功だけを抽出 -
<|>
: 何種類かの処理を試行
謝辞
Alternativeについては山本先生よりご教示いただきました。
<|> は Alternative 型クラスのメソッドです。import Control.Applicative すれば、Maybe でも <|> は使えます。
— 山本和彦 (@kazu_yamamoto) 2014年10月6日
Alternative は制限の緩い MonadPlus です。Monad が掛け算、MonadPlus が足し算の系を表します。MonadPlus はもう古いので、忘れて Alternative を使って下さい。
— 山本和彦 (@kazu_yamamoto) 2014年10月6日
ちなみに Parsec の <|> は、Alternative ではなく、独自実装だったと思います。
— 山本和彦 (@kazu_yamamoto) 2014年10月6日
Alternative に関しては、このスライドを見ると、頭なの中がすっきりするかもしれません。 http://mew.org/~kazu/material/2012-monoid.pdf
— 山本和彦 (@kazu_yamamoto) 2014年10月6日