HaskellのEitherについて
HaskellのEitherモナドとそれに関連するData.Eitherの関数についてまとめました。この記事で使われているGHCのバージョンは8.8.3です。
Eitherとは
Eitherとは、Haskellに標準で入っているモナド・型の1つです。GHCiで定義を見てみましょう。
data Either a b = Left a | Right b -- Defined in ‘Data.Either’
GHCiによると、Eitherは2つの値をとり、それぞれ違う2つの状態をとりえる、ということです。例外処理の際、失敗した計算を扱えます。Maybeモナドでは失敗した場合Nothing
が返るだけで詳しい情報が得られませんが、Eitherでは失敗をより詳細に表現できます。
失敗 | 成功 | |
---|---|---|
Maybe a |
Nothing |
Just a |
Either a b |
Left a |
Right b |
失敗した場合はLeft a
、成功すればRight b
として値を返します。
Eitherで遊んでみる
ghcir> :t Right 1011
Right 1011 :: Num b => Either a b
ghci> :t Left "aaa"
Left "aaa" :: Either [Char] b
Eitherの使いどころ
失敗しただけではなく、失敗した際の情報を得る(または処理を中断させない)ために使います。
Prelude.head
を例にとります。
headはリストを引数にとり、先頭の要素を返します。ただし、リストが空だと*** Exception: Prelude.head: empty list
という例外が発生します。
ghci> head "abcdef"
'a'
ghci> head [1,4,5,2]
1
ghci> head []
`*** Exception: Prelude.head: empty list`
これをそのまま実装すると、以下のコードになります。
head :: [a] -> a
head [] = errorWithoutStackTrace "empty list"
head (x:xs) = x
これをEitherを使って実装してみます。
head' :: [a] -> Either String a
head' [] = Left "empty list"
head' (x:xs) = Right x
正しく動作しているのがわかります。
ghci> head' [1,3,2]
Right 1
ghci> head' "aaaaa"
Right 'a'
ghci> head' []
Left "empty list"
このRight x
からx
を取り出す方法はData.Eitherで紹介します。
Data.Either
EitherはData.Eitherで定義されています。Data.Eitherには、以下のようなEitherを扱う関数があります。
either :: (a -> c) -> (b -> c) -> Either a b -> c
fromLeft :: a -> Either a b -> a
fromRight :: b -> Either a b -> b
isLeft :: Either a b -> Bool
isRight :: Either a b -> Bool
lefts :: [Either a b] -> [a]
partitionEithers :: [Either a b] -> ([a], [b])
rights :: [Either a b] -> [b]
1つ1つ見ていきましょう。
either
either :: (a -> c) -> (b -> c) -> Either a b -> c
either
はPreludeに含まれていて、関数を2つとEither a b型の値をとります。引数の値がLeft
かRight
で処理を分けたいときに使えます。
ghci> let x = Right 1234 :: Either String Int
ghci> let y = Left "left" :: Either String Int
ghci> either length (*3) x
3702
ghci> either length (*3) y
4
ちなみに、1行目と2行目の束縛での型注釈は必須ではありません。型注釈をつけなかった場合、xとyの型は以下のようになります。
x :: Num b => Either a b
y :: Either [Char] b
このとき、xのEither a
とyのEither b
の型をコンパイラは知りませんが、このままeither length (*3)
に突っ込んでもエラーは発生せず、上と同じ結果が得られます。なぜなら、eitherは値がLeftかRightかわかった時点でもう片方の関数は評価しないからです。なので、eitherがとる値はLeftかRightであればよいのです。
fromLeft, fromRight
fromLeft :: a -> Either a b -> a
fromRight :: b -> Either a b -> b
fromLeftはLeftから、fromRightはRightから値を取り出します。fromLeftはEitherの値がRightだった時の値を、fromRightはEitherの値がLeftだった時の値をそれぞれとります。ただし、2つの引数の型は同じ(Left Int → Int、Right String → String)である必要があります。
ghci> fromLeft "bananas" (Left "apple")
"apple"
ghci> fromLeft 493 (Right 34141)
493
isLeft, isRight
isLeft :: Either a b -> Bool
isRight :: Either a b -> Bool
それぞれ、引数の型がLeft、RightであればTrue、そうでなければFalseを返します。
ghci> isLeft (Left "aaa")
True
ghci> isRight (Right 381)
True
lefts, rights
lefts :: [Either a b] -> [a]
rights :: [Either a b] -> [b]
Eitherのリストから、それぞれLeft、Rightの値をフィルタします。
ghci> let ls = [Right 3,Left "apple",Right 4, Left "bananas",Right 10]
ghci> lefts ls
["apple","bananas"]
ghci> rights ls
[3,4,10]
partitionEithers
partitionEithers :: [Either a b] -> ([a], [b])
lefts、rightsで取り出した値を、1つのタプルに格納します。
ghci> partitionEithers ls
(["apple","bananas"],[3,4,10])
EitherとFunctor、Applicative
もうお気づきの方もいると思いますが、Eitherは実はFunctorのインスタンスです。1
instance Functor (Either a) -- Defined in ‘Data.Either’
ghc/libraries/base/Data/Either.hs
に定義があります。
instance Functor (Either a) where
fmap _ (Left x) = Left x
fmap f (Right y) = Right (f y)
fmapにRightの値を与えると関数が適用され、Leftの値を与えるとそのまま返ってくる、ということです。
ghci> fmap (*3) (Left 2)
Left 2982
ghci> fmap (*3) (Right 2)
Right 6
ghci> (*3) <$> Right 4
Right 12
ghci> (*3) <$> Left 4
Left 4
また、EitherはApplicativeのインスタンスでもあるので、アプリカティブ・スタイルでコードを書くこともできます。
instance Applicative (Either e) where
pure = Right
Left e <*> _ = Left e
Right f <*> r = fmap f r
pureの引数はRightで返ってきます。<*>
の右辺の値がLeftならそれが返ってきて、Rightなら関数が適用されて返ってきます。
ghci> pure 3 :: Either String Int
Right 3
ghci> Right (*3) <*> Left 3
Left 3
ghci> Right (*3) <*> Right 3
Right 9
ghci> Left 3 <*> Right 3
Left 3
ghci> Left 3 <*> Left 3
Left 3
記事の先頭で書いたとおり、EitherはモナドなのでMonadのインスタンスとして定義があります。
instance Monad (Either e) where
Left l >>= _ = Left l
Right r >>= k = k r
>>=
にLeftの値を突っ込むとそのまま、Rightの値は関数が適用されて返ってくる、ということです。
Functor、Applicative、MonadそれぞれLeftなら値がスルーされ、Rightなら評価されるという認識でよいかと思います。
参考文献
注釈
-
Functor、Applicative、Monadについては上の記事が非常にわかりやすいので読んでいただければと思います。 ↩