※この記事のMaybe編はこちら
idと名前のデータベースと、順位とidのデータベースからなる、下記のようなフレームワークを考えてみよう。
2位の名前を取り出すには、idFromRank で 2位のidを取り出し、その id を nameFromId に渡して名前を得るものとする。
idFromRank も nameFromId も要求された順位やidのデータがなければ Left に評価されるものとする。
type Err = String type ID = Int type Rank = Int type Name = String type RankDB = [(Rank,ID)] type NameDB = [(ID,Name)] idFromRank :: RankDB -> Rank -> Either Err ID idFromRank db rk = case lookup rk db of Nothing -> Left $ "id of rank " ++ (show rk) ++ " was not found." Just id -> Right id nameFromId :: NameDB -> ID -> Either Err Name nameFromId db id = case lookup id db of Nothing -> Left $ "name of id " ++ (show id) ++ " was not found." Just name -> Right name
このフレームワークを使って、上位3位の名前を取得する関数を作成してみよう。
ポイントは、idFromRank も nameFromId も Left に評価されることがあるため、この関数もEither型に評価されるものとし、1位から3位までのidがすべて存在し、それらの名前がすべて存在する場合のみRightに評価され、それ以外のときはLeftに評価されるようにすることだ。
下記のように呼び出されるようになればよいだろう。
main = do print $ topThree [(1,101),(2,102),(3,103)] [(101,"1st"),(102,"2nd"),(103,"3rd")] print $ topThree [(1,101),(4,102),(3,103)] [(101,"1st"),(102,"2nd"),(103,"3rd")] print $ topThree [(1,101),(2,102),(3,103)] [(101,"1st"),(104,"4th"),(103,"3rd")]
式を使ったプログラミングで素直に記述する場合、筆者の能力では下記の記述が限界である。
これはどう考えても地獄である。我々が関数プログラミングに求めていたものはこんなものではなかったはずだ。
topThree :: RankDB -> NameDB -> Either Err (Name,Name,Name) topThree rdb ndb = let eitherId1 = idFromRank rdb 1 in if isLeft eitherId1 then Left $ fromLeft eitherId1 else let eitherName1 = nameFromId ndb $ fromRight eitherId1 in if isLeft eitherName1 then Left $ fromLeft eitherName1 else let eitherId2 = idFromRank rdb 2 in if isLeft eitherId2 then Left $ fromLeft eitherId2 else let eitherName2 = nameFromId ndb $ fromRight eitherId2 in if isLeft eitherName2 then Left $ fromLeft eitherName2 else let eitherId3 = idFromRank rdb 3 in if isLeft eitherId3 then Left $ fromLeft eitherId3 else let eitherName3 = nameFromId ndb $ fromRight eitherId3 in if isLeft eitherName3 then Left $ fromLeft eitherName3 else Right ( fromRight eitherName1, fromRight eitherName2, fromRight eitherName3 ) where fromLeft (Left x) = x fromRight (Right x) = x
Haskell には case 文があるので、効果的に使えば下記のように改善することができる。
しかし case 文の入れ子も本質的には地獄である。こんなことなら手続き型言語を使えばよいのではないか。
topThreeC :: RankDB -> NameDB -> Either Err (Name,Name,Name) topThreeC rdb ndb = case idFromRank rdb 1 of Left e -> Left e Right id1 -> case nameFromId ndb id1 of Left e -> Left e Right n1 -> case idFromRank rdb 2 of Left e -> Left e Right id2 -> case nameFromId ndb id2 of Left e -> Left e Right n2 -> case idFromRank rdb 3 of Left e -> Left e Right id3 -> case nameFromId ndb id3 of Left e -> Left e Right n3 -> Right (n1,n2,n3)
そこで Eitherモナド登場。
Either型のモナドとしての性質を使うと、上記とまったく同じ意味の関数を下記の内容だけで記述することができる。
モナドとしてのEither型には、失敗するかもしれない計算どうしを組み合わせるときは、一部でも失敗したらすべてが失敗したことにする
という性質が最初から組み込まれているためである。
topThreeM :: RankDB -> NameDB -> Either Err (Name,Name,Name) topThreeM rdb ndb = do id <- idFromRank rdb 1 n1 <- nameFromId ndb id id <- idFromRank rdb 2 n2 <- nameFromId ndb id id <- idFromRank rdb 3 n3 <- nameFromId ndb id return (n1,n2,n3)
全部のせておく。
import Data.Either (isLeft) type Err = String type ID = Int type Rank = Int type Name = String type RankDB = [(Rank,ID)] type NameDB = [(ID,Name)] idFromRank :: RankDB -> Rank -> Either Err ID idFromRank db rk = case lookup rk db of Nothing -> Left $ "id of rank " ++ (show rk) ++ " was not found." Just id -> Right id nameFromId :: NameDB -> ID -> Either Err Name nameFromId db id = case lookup id db of Nothing -> Left $ "name of id " ++ (show id) ++ " was not found." Just name -> Right name topThree :: RankDB -> NameDB -> Either Err (Name,Name,Name) topThree rdb ndb = let eitherId1 = idFromRank rdb 1 in if isLeft eitherId1 then Left $ fromLeft eitherId1 else let eitherName1 = nameFromId ndb $ fromRight eitherId1 in if isLeft eitherName1 then Left $ fromLeft eitherName1 else let eitherId2 = idFromRank rdb 2 in if isLeft eitherId2 then Left $ fromLeft eitherId2 else let eitherName2 = nameFromId ndb $ fromRight eitherId2 in if isLeft eitherName2 then Left $ fromLeft eitherName2 else let eitherId3 = idFromRank rdb 3 in if isLeft eitherId3 then Left $ fromLeft eitherId3 else let eitherName3 = nameFromId ndb $ fromRight eitherId3 in if isLeft eitherName3 then Left $ fromLeft eitherName3 else Right ( fromRight eitherName1, fromRight eitherName2, fromRight eitherName3 ) where fromLeft (Left x) = x fromRight (Right x) = x topThreeC :: RankDB -> NameDB -> Either Err (Name,Name,Name) topThreeC rdb ndb = case idFromRank rdb 1 of Left e -> Left e Right id1 -> case nameFromId ndb id1 of Left e -> Left e Right n1 -> case idFromRank rdb 2 of Left e -> Left e Right id2 -> case nameFromId ndb id2 of Left e -> Left e Right n2 -> case idFromRank rdb 3 of Left e -> Left e Right id3 -> case nameFromId ndb id3 of Left e -> Left e Right n3 -> Right (n1,n2,n3) topThreeM :: RankDB -> NameDB -> Either Err (Name,Name,Name) topThreeM rdb ndb = do id <- idFromRank rdb 1 n1 <- nameFromId ndb id id <- idFromRank rdb 2 n2 <- nameFromId ndb id id <- idFromRank rdb 3 n3 <- nameFromId ndb id return (n1,n2,n3) main = do print $ topThree [(1,101),(2,102),(3,103)] [(101,"1st"),(102,"2nd"),(103,"3rd")] print $ topThree [(1,101),(4,102),(3,103)] [(101,"1st"),(102,"2nd"),(103,"3rd")] print $ topThree [(1,101),(2,102),(3,103)] [(101,"1st"),(104,"4th"),(103,"3rd")] print $ topThreeC [(1,101),(2,102),(3,103)] [(101,"1st"),(102,"2nd"),(103,"3rd")] print $ topThreeC [(1,101),(4,102),(3,103)] [(101,"1st"),(102,"2nd"),(103,"3rd")] print $ topThreeC [(1,101),(2,102),(3,103)] [(101,"1st"),(104,"4th"),(103,"3rd")] print $ topThreeM [(1,101),(2,102),(3,103)] [(101,"1st"),(102,"2nd"),(103,"3rd")] print $ topThreeM [(1,101),(4,102),(3,103)] [(101,"1st"),(102,"2nd"),(103,"3rd")] print $ topThreeM [(1,101),(2,102),(3,103)] [(101,"1st"),(104,"4th"),(103,"3rd")]