一.Functor から Monad へ
タイプから見ると、Functor から Applicative を経て Monad へは一般から特殊への段階的プロセスです(Monad は特殊な Applicative、Applicative は特殊な Functor)
Functor
通常の関数を context を持つ値に map over できます
fmap :: (Functor f) => (a -> b) -> f a -> f b
context 関連計算の中で最もシンプルなシーンを解決するために使用:context を持たない関数を context を持つ値にどのように適用するか?
(+1) ->? Just 1
fmap の登場:
> fmap (+1) (Just 1)
Just 2
Applicative
Functor 上の強化で、context 内の関数を context を持つ値に map over できます
(<*>) :: (Applicative f) => f (a -> b) -> f a -> f b
pure :: (Applicative f) => a -> f a
Applicative は計算文脈(computation context)と理解できます。Applicative 値は計算です。例えば:
Maybe aは失敗する可能性のある computation を表し、[a]は同時に多くの結果がある computation(non-deterministic computation) を表し、IO aは side-effects がある computation を表します。
P.S.computation context に関する詳細情報は、[Functor と Applicative_Haskell ノート 7](/articles/functor と applicative-haskell ノート 7/#articleHeader4) を参照
context 関連計算の中の別のシーンを解決するために使用:context を持つ関数を context を持つ値にどのように適用するか?
Just (+1) ->? Just 1
<*> の登場:
> Just (+1) <*> (Just 1)
Just 2
Monad
Applicative 上の強化で、通常の値を入力し context を持つ値を出力する関数を、context を持つ値に適用できます
(>>=) :: (Monad m) => m a -> (a -> m b) -> m b
context を持つ値
m aがある場合、それを通常の値aのみを受け取る関数に投げ込み、context を持つ値を返すにはどうすればよいでしょうか?つまり、型がa -> m bの関数をm aにどのように適用するか?
context 関連計算の中の最後のシーンを解決するために使用:通常の値を入力し context を持つ値を出力する関数を、context を持つ値にどのように適用するか?
\x -> Just (x + 1) ->? Just 1
>>= の登場:
> Just 1 >>= \x -> Just (x + 1)
Just 2
三者の関連
インターフェースの動作から見ると、これら 3 つはすべて context を持つ値と関数を中心に事を搞しています(つまり、context 関連の計算)。では、考えてみてください。共有する組み合わせ状況は何種類ありますか?
-
関数の入力出力タイプが一致する状況
-
context 内の関数 + context 内の値:
Applicative -
context 内の関数 + 通常の値:
pureで包んでから呼び出す -
通常の関数 + context 内の値:
Functor -
通常の関数 + 通常の値:関数呼び出し
-
-
関数の入力出力タイプが一致しない状況
-
関数は通常の値を入力し context 内の値を出力 + context 内の値:
Monad -
関数は通常の値を入力し context 内の値を出力 + 通常の値:直接呼び出す
-
関数は context 内の値を入力し通常の値を出力 + context 内の値:直接呼び出す
-
関数は context 内の値を入力し通常の値を出力 + 通常の値:
pureで包んでから呼び出す
-
したがって、このシーン(context 内にあるかどうかの関数を context 内にあるかどうかの値に適用する)に関して言えば、Functor、Applicative および Monad を���っていればすべての状況に対応するのに十分です
二.Monad typeclass
class Applicative m => Monad m where
(>>=) :: forall a b. m a -> (a -> m b) -> m b
(>>) :: forall a b. m a -> m b -> m b
m >> k = m >>= \_ -> k
return :: a -> m a
return = pure
fail :: String -> m a
fail s = errorWithoutStackTrace s
実際には、Monad インスタンスは >>= 関数(bind と呼ばれる)を実装するだけでよいです。言い換えれば、Monad は >>= 操作をサポートする Applicative functor に過ぎません
return は pure の別名なので、依然として通常の値を受け取り、それを最小の context に入れます(通常の値を Monad の中に包む)
(>>) :: m a -> m b -> m b はデフォルト実装を定義し、関数 \_ -> m b を >>= を通じて m a に適用し、(チェーン操作中に)前の計算結果を無視するために使用します
P.S. チェーン操作中、遭遇する >> を >>= \_ -> に置き換えれば簡単に理解できます
P.S. 上記のタイプ宣言中の forall は ∀ を指します(離散数学中の量詞、全称量詞 ∀ は「任意」を表し、存在量詞 ∃ は「存在」を表します)。したがって forall a b. m a -> (a -> m b) -> m b は、任意のタイプ変数 a と b に対して、>>= 関数のタイプは m a -> (a -> m b) -> m b であることを言います。forall a b. を省略できます。デフォルトですべての小文字タイプパラメータは任意だからです:
In Haskell, any introduction of a lowercase type parameter implicitly begins with a forall keyword
三.Maybe Monad
Maybe の Monad 実装は非常に直感的に合致します:
instance Monad Maybe where
(Just x) >>= k = k x
Nothing >>= _ = Nothing
fail _ = Nothing
>>= は関数 k を Just 内の値に適用し、結果を返します。Nothing の場合は、直接 Nothing を返します。例えば:
> Just 3 >>= \x -> return (x + 1)
Just 4
> Nothing >>= \x -> return (x + 1)
Nothing
P.S. 私たちが提供する関数 \x -> return (x + 1) に注意。return の価値が現れました。関数タイプは a -> m b であることを要求するので、結果を return で包むのは非常に便利で、セマンティクスも非常に適切です
この特性は一連のエラーが発生する可能性のある操作を処理するシーンに非常に適しています。例えば JS の:
const err = error => NaN;
new Promise((resolve, reject) => {
resolve(1)
})
.then(v => v + 1, err)
.then(v => {throw v}, err)
.then(v => v * 2, err)
.then(console.log.bind(this), err)
一連の操作で、中間ステップでエラーが発生する可能性があり(throw v)、エラーが発生した後はエラーを表す結果(上記の例では NaN)を取得し、エラーが発生しなければ正しい結果を取得できます
Maybe の Monad 特性を使用して記述:
> return 1 >>= \x -> return (x + 1) >>= \_ -> (fail "NaN" :: Maybe a) >>= \x -> return (x * 2)
Nothing
1:1 で完全に復元し、Maybe Monad を利用して一連のエラーが発生する可能性のある操作に从容に対応
四.do 表示法
I/O シーンで do 文ブロックを使用したことがあります(do-notation と呼ばれる)。一連の I/O Action を組み合わせることができます。例えば:
> do line <- getLine; char <- getChar; return (line ++ [char])
hoho
!"hoho!"
3 つの I/O Action をつなぎ、最後の I/O Action を返しました。実際には、do 表示法は I/O シーンだけでなく、任意の Monad にも適用されます
構文に関して言えば、do 表示法は各行が monadic value でなければなりません。なぜでしょうか?
do 表示法は >>= の構文糖に過ぎないからです。例えば:
foo = do
x <- Just 3
y <- Just "!"
Just (show x ++ y)
context を涉及しない通常の計算と比較:
let x = 3; y = "!" in show x ++ y
do 表示法の爽やかで簡潔な優位性を発見するのは難しくありません。実際には:
foo' = Just 3 >>= (\x ->
Just "!" >>= (\y ->
Just (show x ++ y)))
do 表示法がなければ、手動で一堆の lambda ネストを書く必要があります:
Just 3 >>= (\x -> Just "!" >>= (\y -> Just (show x ++ y)))
したがって <- の作用は:
>>=を使用して monadic value を lambda に持ってくるのと同じ
>>= があります。では >> は?どのように使用しますか?
maybeNothing :: Maybe Int
maybeNothing = do
start <- return 0
first <- return ((+1) start)
Nothing
second <- return ((+2) first)
return ((+3) second)
do 表示法で演算の行を書いたが、
<-を使用して値をバインドしない場合、実際には>>を使用したことになります。彼は計算結果を無視します。私たちは彼らの順序を必要とするだけで、彼らの結果を必要とするのではなく、_ <- Nothingと書くよりずっと綺麗です。
最後に、fail があります。do 表示法でエラーが発生すると自動的に fail 関数を呼び出します:
fail :: String -> m a
fail s = errorWithoutStackTrace s
デフォルトではエラーを報告し、プログラムを停止させます。具体的な Monad インスタンスには独自の実装があります。例えば Maybe:
fail _ = Nothing
エラーメッセージを無視し、Nothing を返します。试玩一下:
> do (x:xs) <- Just ""; y <- Just "abc"; return y;
Nothing
do 文ブロック中でパターンマッチが失敗し、直接 fail を返します。意義は:
このようにパターンマッチの失敗は monad の context 中に制限され、プログラム全体の失敗にはなりません
五.List Monad
instance Monad [] where
xs >>= f = [y | x <- xs, y <- f x]
(>>) = (*>)
fail _ = []
List の context は不確定な環境(non-determinism)、つまり複数の結果が存在することを指します。例えば [1, 2] には 2 つの結果(1,2)があり、[1, 2] >>= \x -> [x..x + 2] には 6 つの結果(1,2,3,2,3,4)があります
P.S.「複数の結果」をどのように理解するか?
C 言語を初めて学ぶ時に困惑がありました。関数は複数の
returnを持てるか?ではどのように複数の値を返すか?
配列(または構造体、リンクリストなど)を返すことができます。複数の値を一緒に組織し(データ構造に入れ)、パッケージして返します
もし関数が配列を返すなら、彼がいくつの結果を返したか不確定です。これがいわゆる不確定な環境です
List の Monad 実装から見ると、>>= はマッピング操作��、言うべきことはありません
>> は少し面白く見えます。Applicative 上に定義された *> と等価です:
class Functor f => Applicative f where
(*>) :: f a -> f b -> f b
a1 *> a2 = (id <$ a1) <*> a2
class Functor f where
(<$) :: a -> f b -> f a
(<$) = fmap . const
const :: a -> b -> a
const x _ = x
作用は最初のパラメータ中の値を破棄し、構造意味(List 長さ情報)のみを保持することです。例えば:
> [1, 2] >> [3, 4, 5]
[3,4,5,3,4,5]
等価于:
> ((fmap . const) id $ [1, 2]) <*> [3, 4, 5]
[3,4,5,3,4,5]
-- または
> [id, id] <*> [3, 4, 5]
[3,4,5,3,4,5]
List Comprehension と do 表示法
面白い例:
> [1,2] >>= \n -> ['a','b'] >>= \ch -> return (n,ch)
[(1,'a'),(1,'b'),(2,'a'),(2,'b')]
最後の n はあまり科学的に見えません(infixl 1 >>= を見るとアクセスできないように見えます)。実際には n にアクセスできます。なぜなら lambda 表現の貪欲マッチ特性のためで、等価于:
[1,2] >>= \n -> (['a','b'] >>= \ch -> return (n,ch))
-- 括弧を追加した完全版
([1, 2] >>= (\n -> (['a','b'] >>= (\ch -> return (n,ch)))))
関数本体に境界がなければ最右端までマッチします。関連討論は Haskell Precedence: Lambda and operator を参照
P.S. さらに、式の結合方式が不確定な場合(どのように括弧を追加するか分からない)は、神奇な方法があります。How to automatically parenthesize arbitrary haskell expressions? を参照
do 表示法で再書き込み:
listOfTuples = do
n <- [1,2]
ch <- ['a','b']
return (n,ch)
形式上は List Comprehension と非常に似ています:
[ (n,ch) | n <- [1,2], ch <- ['a','b'] ]
実際には、List Comprehension と do 表示法はすべて構文糖で、最後はすべて >>= に変換されて計算されます
六.Monad laws
同様に、Monad もいくつかの規則に従う必要があります:
-
左単位元(Left identity):
return a >>= f ≡ f a -
右単位元(Right identity):
m >>= return ≡ m -
結合律(Associativity):
(m >>= f) >>= g ≡ m >>= (\x -> f x >>= g)
単位元の性質はあまり明らかに見えません。Kleisli composition を借助してより標準的な形式に変換できます:
-- | Left-to-right Kleisli composition of monads.
(>=>) :: Monad m => (a -> m b) -> (b -> m c) -> (a -> m c)
f >=> g = \x -> f x >>= g
(Control.Monad から引用)
タイプ宣言から見ると、>=> は*Monad 関数間の組合せ演算*(monadic function)に相当します。これらの関数は通常の値を入力し、monadic 値を出力します。通常の関数組合せと比較:
(.) :: (b -> c) -> (a -> b) -> a -> c
(.) f g = \x -> f (g x)
>=> は左から右に Moand m => a -> m b の関数を組合せ、. は右から左に a -> b の関数を組合せます
P.S. では、右から左の Monad 関数組合せはありますか?そうです、<=< です
Kleisli composition(>=>)を使用して Monad laws を記述:
-
左単位元:
return >=> f ≡ f -
右単位元:
f >=> return ≡ f -
結合律:
(f >=> g) >=> h ≡ f >=> (g >=> h)
この 3 条を満たすので、標準的な Monoid です。Moand m => a -> m b 関数集合及びその上に定義された >=> 演算は幺半群を構成し、幺元は return です
P.S.>=> で記述された Monad laws のより大きな意義は、この 3 条が数学圏を形成するために必要な規律であることで、これから圏の数学的意義を持ちます。詳細は Category theory を参照
MonadPlus
同時に Monad と Monoid を満たすものには専用の名前があり、MonadPlus と呼ばれます:
class (Alternative m, Monad m) => MonadPlus m where
mzero :: m a
mzero = empty
mplus :: m a -> m a -> m a
mplus = (<|>)
List のシーンでは、mzero は [] で、mplus は ++ です:
instance Alternative [] where
empty = []
(<|>) = (++)
これには何の用途がありますか?
例えばリスト要素をフィルタリングする場合、List Comprehension が最もシンプルです:
> [ x | x <- [1..50], '7' `elem` show x ]
[7,17,27,37,47]
>>= を使用しても処理できます:
> [1..50] >>= \x -> if ('7' `elem` show x) then [x] else []
[7,17,27,37,47]
条件表現はやや臃腫に見えます。MonadPlus があればより簡潔で力強い表現方式に置き換えることができます:
> [1..50] >>= \x -> guard ('7' `elem` show x) >> return x
[7,17,27,37,47]
その中で guard 関数は以下の通り:
guard :: (Alternative f) => Bool -> f ()
guard True = pure ()
guard False = empty
ブール値を入力し、context を持つ値を出力します(True はデフォルト context 内の () に対応し、False は mzero に対応)
guard 処理後、>> を利用して非幺元値を元の値に回復し(return x)、幺元は >> 演算後でも幺元([])のままで、フィルタリングされます
対応する do 表示法は以下の通り:
sevensOnly = do
x <- [1..50]
guard ('7' `elem` show x)
return x
List Comprehension 形式と比較:
[ x | x <- [1..50], '7' `elem` show x ]
非常に似ており、すべてほとんど余分な句読点がない簡練な表現です
do 表示法中的作用
Monad laws を do 表示法に置き換えて記述すれば、もう一组の等価変換規則が得られます:
-- Left identity
do { x′ <- return x;
f x′
}
≡
do { f x }
-- Right identity
do { x <- m;
return x
}
≡
do { m }
-- Associativity
do { y <- do { x <- m;
f x
}
g y
}
≡
do { x <- m;
do { y <- f x;
g y
}
}
≡
do { x <- m;
y <- f x;
g y
}
これらの規則には2 つの作用があります:
-
コードを簡素化するために使用可能
skip_and_get = do unused <- getLine line <- getLine return line -- Right identity を利用し、余分な return を削除 skip_and_get = do unused <- getLine getLine -
do block ネストを回避可能
main = do answer <- skip_and_get putStrLn answer -- 展開 main = do answer <- do unused <- getLine getLine putStrLn answer -- 結合律を使用して do block ネストを解く main = do unused <- getLine answer <- getLine putStrLn answer
七.Monad と Applicative
最初のシーンに戻り、私たちは Monad が構文上で context 関連計算を簡素化でき、a -> m b を m a に適用できることを知っています
Monad が Applicative の基礎の上に建立されている既然、では、Applicative と比較して、Monad の核心的な優位性はどこにあり、何によって存在する資格があるのか?
applicative functor は applicative value 間に弾力的な交互を許可しないから
これ、どのように理解するか?
もう一つの Maybe Applicative の例を見る:
> Just (+1) <*> (Just (+2) <*> (Just (+3) <*> Just 0))
Just 6
中間リンクがすべてエラーを出さない Applicative 演算は、正常に結果を取得できます。もし中間リンクでエラーが出たら?
-- 中間で失敗
> Just (+1) <*> (Nothing <*> (Just (+3) <*> Just 0))
Nothing
これも予想に合致します。純 Applicative 演算はすでに需要を満たしているように見えます。よく見てみてください。刚才はどのように中間リンクの失敗を表現したか:Nothing <*> some thing。この Nothing はハードコードで取り付けられた爆弾のようで、純粋な静的シーンです
では動的に爆発したい場合、どうするか?
-- 柔軟性不足
> Just (+1) <*> (Just (\x -> if (x > 1) then Nothing else return (x + 2)) <*> (Just (+3) <*> Just 0))
<interactive>:85:1: error:
? Non type-variable argument in the constraint: Num (Maybe a)
(Use FlexibleContexts to permit this)
? When checking the inferred type
it :: forall a. (Ord a, Num (Maybe a), Num a) => Maybe (Maybe a)
エラーの原因は動的に爆発を制御しようとしたが、Maybe (Maybe a) を作り出してしまったことです:
> Just (\x -> if (x > 1) then Nothing else return (x + 2)) <*> (Just (+3) <*> Just 0)
Just Nothing
このような気まずい局面が現れた理由は、Applicative の <*> が機械的に左側の context から関数を取り出し、右側の context 内の値に適用するだけです。Maybe から関数を取る結果は 2 種類のみ:Nothing から何も取り出せず、直ちに爆発;または Just f から f を取り出し、演算して Just (f x) を取得。前一步(x)が爆発しなければ爆発できません
したがって、应用场景から見ると、Monad は一種の計算文脈制御で、いくつかの通用シーン(例えばエラー処理、I/O、不確定結果数の計算など)に対応します。その存在意義は:Applicative よりも柔軟で、各ステップの計算中に制御を追加することを許可し、Linux パイプラインのようです
コメントはまだありません