Hatena::ブログ(Diary)

いじわるだねっ このページをアンテナに追加 RSSフィード

2012-01-15

[] Maybe

Preludeを上から順に…と思ったら、早速Maybeで心折れそうです。

Maybeについて。

data Maybe a = Nothing | Just a

定義はこんな感じで、Maybeの中は Nothing か Just a のどちらかです。値がある場合はその値(Just a)、ない場合は何もない(Nothing)という感じ。

このNothingは、ある種の失敗を表していて、Javaでいうnullみたいな感じでしょうか。でもnullよりMaybeが良いのは、ちゃんと型にMaybeが付いて失敗の可能性が明らかに分ることや、その処理の楽さでしょうか…。

Maybeと一緒に使うのがmaybe関数です。

maybe :: b -> (a -> b) -> Maybe a -> b

引数にコメントをつけると

maybe :: b          -- デフォルト値
      -> (a -> b)   -- 値があった場合に適用する関数
      -> Maybe a    -- 対象の値
      -> b          -- 戻り値

ということで、例えばですが、ユーザーがすでにログインしていたらログイン名を使い、そうでなければログインリンクを表示するという処理があったとすると…

showName :: Maybe String -> String
showName mname = maybe "ログイン" (\name -> "こんにちは、" ++ name ++"さん") mname

と書けます。まぁこんな感じでパターンマッチしてもいいのですが…

showName2 :: Maybe String -> String
showName2 mname =  
  case mname of
    Nothing   -> "ログイン"
    Just name -> "こんにちは、" ++ name ++ "さん"

maybeの方が短いけど、ちょとみづらいかも。。ところで、Maybe型のデータはよく変数名の頭にmをつけたりします。Maybeから取り出したらmを取ります。

実はMaybeのためだけの関数は少なくて、Haskell標準ライブラリであるPreludeに定義されているのはmaybe関数くらいでしょうか…。もう少しだけData.Maybeライブラリにありますが、すぐ覚えられる程度しかないですね…。

他に値を返したり返さなかったりする関数戻り値がMaybeになっていることがあります。たとえばPrelude内でいうとlookupとか

lookup :: Eq a => a -> [(a, b)] -> Maybe b

これはキーバリューの組のリストから、キーを指定してバリューを取ってくる関数です。戻り値がMaybeなので、さっきのmaybe関数と一緒に使うと便利そうです。

values :: [(String,Int)]
values = [("人参",100), ("大根", 250), ("トマト", 150)]

howMuch :: String -> String
howMuch name = maybe "見つかりません" ((++"円").show) (lookup name values) 

単にデータとしてのMaybeはこんなものです。ところでMaybeのインスタンスの顔ぶれがなかなかすごいですね…。

Monad Maybe 
Functor Maybe 
Typeable1 Maybe 
MonadFix Maybe 
MonadPlus Maybe 
Applicative Maybe 
Foldable Maybe 
Traversable Maybe 
Alternative Maybe 
Eq a => Eq (Maybe a) 
Data a => Data (Maybe a) 
Ord a => Ord (Maybe a) 
Read a => Read (Maybe a) 
Show a => Show (Maybe a) 
Generic (Maybe a) 
Monoid a => Monoid (Maybe a)

MaybeはFunctorのインスタンスになっています。Functorとは簡単に言ってしまうと、値を入れる箱=コンテナのような性質です。Maybeは定義のとおり、値aをコンテナの中に持っています。

data Maybe a = Nothing | Just a
           ↑これ

普通は、さっきみたmaybe関数とかパターンマッチしないと、この中のaには触れないんですが、Functorクラスが唯一提供するfmap関数を使うと、このコンテナに触れずに、中の値に関数を適用することができるわけです。

難しい話は抜きに、実際に使ってみると、たとえば名前があったら何か処理をして、なければ何もしない、というのは…

baka :: Maybe String -> Maybe String
baka mname = fmap (++"は馬鹿") mname

-- こんな結果になる
baka (Just "lostdog")  ---> Just "lostdogは馬鹿"
baka (Nothing)         ---> Nothing 

という感じです。fmapは意外と使いどころがある気がします。

かなり長くなってますが…、MaybeはMonadクラスのインスタンスになっています。モナドとは何か?的な話はよく語られるんですが、定義だけ言ってしまえば、このMonadクラスのインスタンスであるものをモナドと呼びます。この場合はMaybeモナドとか呼びます。

モナドの性質は、一言でいうと、通常のコンテキストに機能を付加するものでしょうか…。Maybeモナドは「失敗するかも」とか「失敗してもいいよ」というような機能を付加します。Maybeモナドを使えば、失敗をうまくハンドリングできるというわけです。

Maybeモナドの実際は、超簡単にいうと、途中でこけたら(値がNothingになったら)、全体の結果もNothing。全部うまくいったら(値がJustだったら)値が返ってくるというものです。

-- Nothingが途中にひとつでも入ったら、全体もNothing
maybeTest1 = Just 1 >> Just 2 >> Nothing >> Just 3  ---> これはNothing

-- 全部がJustのときだけ、Justが返る
maybeTest2 = Just 1 >> Just 2 >> Just 3 >> Just 4  ---> これはJust 4

たとえば、ユーザー入力の処理なんかはそんな感じですね。モナドなのでdo構文が使えます。

-- 全部の入力が必須。ひとつでもNothingだったら、全体がNothing
getInput :: Maybe (String, Int, String)
getInput = do
  name  <- getName
  age   <- getAge
  mail  <- getMail
  return (name, age, mail)

getName :: Maybe String  -- 実装は省略...
getAge  :: Maybe Int
getMail :: Maybe String

失敗のハンドリングがコードとしては書かれていませんが、Maybeモナド内では失敗は自動でハンドリングされるので、わざわざ書かなくてよいわけです(そういう機能がコンテキストに追加されたわけなので)

追記


モノイドという性質が結構役に立つよと言われはじめたのは、いつからかは忘れましたが、そんなに古くなかったはずで、、というわけで、モノイドというのは大雑把にいうと、次のような性質です。

  • 足し算できる
  • その足し算の0となるものがある

どうも最近はMonadにモノイドを足してMonadPlusを作るより、Applicativeにモノイドを足してAlternativeという流れのほうが好まれるっぽいそうなので、Alternativeクラスの説明を入れておきます。

class Applicative f => Alternative f where
    empty :: f a                   -- 0に相当するもの
    (<|>) :: f a -> f a -> f a     -- 足し算に相当するもの

Alternativeの定義は上のとおりです。そしてMaybe型はもちろんこのインスタンスになっています。

instance Alternative Maybe where
    empty = Nothing
    Nothing <|> p = p
    Just x <|> _ = Just x

Alternativeの名のとおり、失敗したら次へフェイルオーバーするというような感じですね…。最初にJustが見つかるまで、頭から見ていくということなので

-- これをimport
import Control.Applicative

Nothing <|> Nothing <|> Just 2 <|> Just 3 <|> Nothing   -- Just 2 が返る
Just 3 <|> Just 2 <|> Just 1                            -- Just 3 が返る

Nothing <|> Just 1  -- この2つは同じこと
empty   <|> Just 1  

という感じです。モノイドとしての性質はMonadPlusと同じなので、ほかの例は下の補足を参考ということで。。


補足

補足のMonadPlusです。これはMonadにモノイドを足したクラスで、名前のとおり、足し算ができるという性質です(正確にはモノイドという性質です、ただモノイドの性質だけならば、別にモナドを前提にする必要がないのですが…)

mzero  :: m a                -- 足し算の零元
mplus  :: m a -> m a -> m a  -- 足し算

MaybeのMonadPlusのインスタンス定義はこんな感じです。

instance MonadPlus Maybe where
   mzero = Nothing

   Nothing `mplus` ys  = ys  -- 左がNothingなら、右を返す
   xs      `mplus` _ys = xs  -- 左がJustなら、左を返す

ということは

-- mplusを使うときはこれをimport
import Control.Monad

-- 左から最初に見つかったJustを返す. これはJust 2が返る
plusTest1 = Nothing `mplus` Just 2 `mplus` Just 3 

-- 全部Nothingだったら、結果もNothing
plusTest2 = Nothing `mplus` Nothing `mplus` Nothing

最初はこんなのいつ使うの?という感じだったみたいですが、有用な例がいくつか提示されたりして、結構使えるじゃんということになってるようです…。

たとえば、ユーザーの連絡先が複数あるとして、優先順位の高い順に探して最初に見つかったところに連絡するとすると…

-- email1, email2, phone の順で連絡先を探して、最初に見つかったものを返す
contact :: Maybe String
contact = getEmail1 `mplus` getEmail2 `mplus` getPhone

-- 実はmsum関数を使うと同じことができる
contact2 = msum [getEmail1, getEmail2, getPhone]

getEmail1, getEmail2, getPhone :: Maybe String -- 実装は省略

という具合です。Maybeはよく使うので少し長くなってしまいましたが、大体こんなところかと思います…。これでMaybeマスターだぜ!

山本和彦山本和彦 2012/01/25 12:49 MonadPlus よりも Applicative を教える方がいいでしょう。両者は本質的に同じなので、Applicative を推奨していきませんか? `mplus` ではなく <|> です!

john-tomioka-michealjohn-tomioka-micheal 2012/01/28 16:23 就活用のブログ作ったわ!
今度見に来て下さい。
URLはメッセか何かで

Lost_dogLost_dog 2012/02/08 01:04 >山本さん
コメントありがとうございます!遅くなりました、、alternativeですよね、使ったことがなかったのでよく知らなかったんですが、こっちのほうがお勧めことなので、入れ替えておきました!

>johnさん
おk

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

トラックバック - http://d.hatena.ne.jp/Lost_dog/20120115/1326653279