top page > computer > haskell > web_lecture > for_programmer > not_monad_applicative.html
更新日:
文責: 重城良国

アプリカティブファンクターであるがモナドではない例

(工事中 60%)

nma4.hs

はじめに

モナドのほうがアプリカティブよりも制約が強い。つまり、モナドのなかまになるためにはより多くの共通部分を要求される。ある構造をモナドにしたときとアプリカティブにしたときとではモナドにしたほうがより共通部分が広くなる。そのぶんだけその構造独自の機能を持つ余地は狭くなる。失敗の可能性のある計算にエラーメッセージを追加する例でこのことを見ていこう。

モナドMaybe

Maybeモナドについてもう一度見てみよう。モナドにするためには以下の関数を定義する。

mret :: a -> m a
mret = Just

mbind :: Maybe a -> (a -> Maybe b) -> Maybe b
mbind m f = maybe Nothing f m

0除算を行わない安全な除算関数を作成する。

safeDivM :: Int -> Int -> Maybe Int
safeDivM _ 0 = Nothing
safeDivM x y = Just $ x `div` y

整数演算a / b + c / dを安全に計算する関数を作成する。

calcM :: Int -> Int -> Int -> Int -> Maybe Int
calcM a b c d =
a `safeDivM` b `mbind` \x ->
c `safeDivM` d `mbind` \y ->
mret $ x + y

試してみる。

% ghci nma4.hs
*Main> calcM 10 3 30 7
Just 7
*Main> calcM 25 4 18 0
Nothing
*Main> calcM 38 0 95 8
Nothing
*Main> calcM 8 0 2 0
Nothing

アプリカティブスタイル

関数calcMはa `safeDivM` bのなかみで変数xを束縛し、c `safeDivM` dのなかみで変数yを束縛し、x + yに文脈をつけて返すという形だ。

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

のような関数を定義することで

calcM a b c d = mret (+) `mapp` (a `safeDivM` b) `mapp` (c `safeDivM` d)

のように書くことができる。これはより直接的に関数(+)の第1引数に(a `safeDivM` b)のなかみをあたえ、第2引数に(c `safeDivM` d)のなかみをあたえていると考えられる。関数mappは

mapp mf mx =
mf `mbind` \f ->
mx `mbind` \m ->
mret $ f m

のように定義できる。文脈つきの関数mfのなかみで変数fを束縛し、文脈つきの値mxのなかみで変数xを束縛し、値xに関数fを適用したものに関数mretで文脈をつけている。

モナドTryM

Maybe値ではエラーをNothingで表現した。これだと何がエラーだったのかわからない。Nothingではなく文字列を持つ値構築子とすることでどんなエラーだったかを文字列で表現することができる。

data TryM a = ErrorM String | SuccessM a deriving Show

これに対してモナド関数を定義する。

tret :: a -> TryM a
tret = SuccessM

tbind :: TryM a -> (a -> TryM b) -> TryM b
SuccessM x `tbind` f = f x
ErrorM em `tbind` _ = ErrorM em

関数mbindのNothingの部分がErrorM emになっている。安全なわり算を定義する。

safeDivTM :: Int -> Int -> TryM Int
safeDivTM x 0 = ErrorM $ show x ++ " is divided by zero\n"
safeDivTM x y = SuccessM $ x `div` y

アプリカティブスタイルで書くための補助関数tmappを定義する。

tmapp :: TryM (a -> b) -> TryM a -> TryM b
tf `tmapp` tx =
tf `tbind` \f ->
tx `tbind` \x ->
tmret $ f x

これらを使って安全なa / b + c / dを定義する。

calcTM :: Int -> Int -> Int -> Int -> TryM Int
calcTM a b c d = tret (+) `tmapp` (a `safeDivTM` b) `tmapp` (c `safeDivTM` d)

試してみる。

*Main> :reload
*Main> calcTM 15 4 28 3
SuccessM 12
*Main> calcTM 8 0 53 8
ErrorM "8 is divided by zero\n"
*Main> calcTM 33 9 25 0
ErrorM "25 is divided by zero\n"
*Main> calcTM 23 0 18 0
ErrorM "23 is divided by zero\n"

はじめに0除算を行ったところで処理が終了してエラーメッセージが返される。

すべてのエラーメッセージを表示したい

モナドではなくアプリカティブなら可能

TryM型を使うと0除算を行う計算が複数あってもはじめのエラーのみが報告される。すべてのエラーメッセージを表示するためにはどうしたらいいだろうか。モナド関数bindの型を見る。

bind :: m a -> (a -> m b) -> m b

エラーが生じるということはm aの型aの値が計算できなかったということだ。よって型aの値を(a -> m b)型の関数にわたすことができない。よってm b型の値は取得できない。たとえエラーであるとしてもふたつめのエラーはm b型の値である。m b型の値が作れないということはエラー値であっても取得できないということだ。

今回の例であるa / b + c / dは直接的にはそれぞれの関数appを使っていて、関数bindを直接使う必要はない。よってアプリカティブ関数を直接定義することを考える。アプリカティブ関数appの型は以下のようになる。

app :: f (a -> b) -> f a -> f b

これであればf (a -> b)型とf a型の値の両方がエラー値であったときに、f b型の値に両方のエラー値の情報を含めることができる。

今回の例を書いてみる

アプリカティブ関数を定義するバージョンを書いてみよう。データ構造は

data TryA a = ErrorA String | SuccessA a deriving Show

となる。アプリカティブ関数は

tpure :: a -> TryA a
tpure = SuccessA

tapp :: TryA (a -> b) -> TryA a -> TryA b
SuccessA f `tapp` SuccessA x = SuccessA $ f x
SuccessA _ `tapp` ErrorA em' = ErrorA em'
ErrorA em `tapp` SuccessA _ = Error A em
ErrorA em `tapp` ErrorA em' = ErrorA $ em ++ em'

関数と値の両方がSuccess値であれば適用した結果をSuccess値として返す。どちらかの値がError値ならばError値を返す。もしも両方の値がError値ならばエラーメッセージを足し合わせる。

安全なわり算は

safeDivTA :: Int -> Int -> TryA Int
safeDivTA x 0 = ErrorA $ show x ++ " is divided by zero\n"
safeDivTA x y = SuccessA $ x `div` y

となる。例の計算は

calcTA :: Int -> Int -> Int -> Int -> TryA Int
calcTA a b c d = tpure (+) `tapp` (a `safeDivTA` b) `tapp` (c `safeDivTA` d)

だ。試してみよう。

*Main> :reload
*Main> calcTA 15 3 19 7
SuccessA 7
*Main> calcTA 55 0 28 3
ErrorA "55 is divided by zero\n"
*Main> calcTA 38 4 17 0
ErrorA "17 is divided by zero\n"
*Main> calcTA 40 0 21 0
ErrorA "40 is divided by zero\n21 is divided by zero\n"

両方のエラーメッセージが結合されて返されていることがわかる。

アプリカティブTryAでできないこと

TryAではエラーが生じたときに、すべてのエラーメッセージを結合して返すことができる。しかし、エラーが生じうる関数からの値を次のエラーが生じうる関数にあたえることができない。TryMでしかできない計算の例は

calc2 :: Int -> Int -> Int -> TryM Int
calc2 a b c =
a `safeDivTM` b `tbind` \x ->
x `safeDivTM` c

となる。f a型の値のなかみのa型の値を(a -> f b)型の関数に渡す必要がある。アプリカティブ関数appの型は

app :: f (a -> b) -> f a -> f b

であり、どう変形してもa型の値を(a -> f b)型に渡すことはできない。例の関数を試してみよう。

*Main> calc2 52 3 4
SuccessM 4
*Main> calc2 33 0 8
ErrorM "2 is divided by zero\n"
*Main> calc2 46 3 0
ErrorM "15 is divided by zero\n"

まとめ

モナドにすることのできないアプリカティブを見た。f a型の値においてなかみにa型の値が存在しないような可能性があり、そのような場合にも「エラー値」のなかみを使って計算を続行するようなアルゴリズムではモナドにすることができない。

メモ

MaybeモナドとErrorモナドではモナド的な使いかたとアプリカティブ的な使いかたを示す。Errorアプリカティブではアプリカティブ的な使いかたを示す。アプリカティブを使ったほうがエラー報告という機能ではより高機能となっている。しかし、計算の結果によってエラーが生じるかどうかを決定する機能はなくなっている。

用語「モナド関数」をreturn, (>>=)やそれと同じ型を持ちモナド則を満たす関数として定義する。用語「アプリカティブ関数」も同様にpure, (<*>)について定義する。

「ファンクターとモナドのあいだ」へもどる 「型クラス: Applicative」へ

正当なCSSです! HTML5 Powered with CSS3 / styling, and Semantics