メインコンテンツへ移動

Monad_Haskell ノート 10

無料2018-06-23#Functional_Programming#Haskell Monad#Haskell do notation#JavaScript Monad#Haskell单子#Applicative and Monad

3 年前に初めて monadic を聞き、好奇心が木に育つまで

一.Functor から Monad へ

タイプから見ると、Functor から Applicative を経て Monad へは一般から特殊への段階的プロセスです(Monad は特殊な ApplicativeApplicative は特殊な 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 内にあるかどうかの値に適用する)に関して言えば、FunctorApplicative および 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 に過ぎません

returnpure の別名なので、依然として通常の値を受け取り、それを最小の 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 は、任意のタイプ変数 ab に対して、>>= 関数のタイプは 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

MaybeMonad 実装は非常に直感的に合致します:

instance  Monad Maybe  where
  (Just x) >>= k      = k x
  Nothing  >>= _      = Nothing
  fail _              = Nothing

>>= は関数 kJust 内の値に適用し、結果を返します。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)を取得し、エラーが発生しなければ正しい結果を取得できます

MaybeMonad 特性を使用して記述:

> 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 を持てるか?ではどのように複数の値を返すか?

配列(または構造体、リンクリストなど)を返すことができます。複数の値を一緒に組織し(データ構造に入れ)、パッケージして返します

もし関数が配列を返すなら、彼がいくつの結果を返したか不確定です。これがいわゆる不確定な環境です

ListMonad 実装から見ると、>>= はマッピング操作��、言うべきことはありません

>> は少し面白く見えます。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

同時に MonadMonoid を満たすものには専用の名前があり、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 内の () に対応し、Falsemzero に対応)

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 bm a に適用できることを知っています

MonadApplicative の基礎の上に建立されている既然、では、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 パイプラインのようです

参考資料

コメント

コメントはまだありません

コメントを書く