メインコンテンツへ移動

Monadic Function_Haskell ノート 12

無料2018-07-06#Functional_Programming#Kleisli Composition#liftM and liftA#Monad Composition#Haskell Monad Functions#Monad Util

いわゆる monadic function とは、monadic value を操作する関数のことです

liftM

liftM :: Monad m => (a1 -> r) -> m a1 -> m r

型宣言から見ると、これはFunctorfmapそのものです:

fmap :: Functor f => (a -> b) -> f a -> f b

context がMonadに変わっただけで、その他に違いはありません。さらに Functor laws と Monad laws を遵守する型の場合、この 2 つの関数は完全に等価です。例えば:

> liftM (+1) (Just 1)
Just 2
> fmap (+1) (Just 1)
Just 2

liftMの具体的な実装は以下の通りです:

liftM   :: (Monad m) => (a1 -> r) -> m a1 -> m r
liftM f m1              = do { x1 <- m1; return (f x1) }

これは以下と等価です:

liftM' f m = m >>= \x -> return (f x)

注意すべき点は、この実装がFunctorの特性に依存せず、Monadが持つ振る舞いだけで完了できることです(さらに Monad laws を遵守すれば、fmapと完全に等価になり、context を持つ値に関数を適用するだけで、余計なことは何もしません)。この観点から見ると、MonadFunctorよりも強力です

MonadFunctorより強力であることは証明済みですが、Applicativeはどうでしょうか?

Applicativeで最も重要なのはこれです:

(<*>) :: Applicative f => f (a -> b) -> f a -> f b

実際にはMonadでも実装でき、apと呼ばれます:

ap :: Monad m => m (a -> b) -> m a -> m b

簡単に実現できます:

mf `ap'` m = do
  f <- mf
  x <- m
  return (f x)

したがって、MonadApplicativeよりも強力です。さらに言えば、カスタムMonadを実装する場合、まずreturn>>=を実装すれば、簡単にApplicative<*> = appure = return)とFunctorfmap = liftM)を実装できます

liftA2liftA3まで)があることは知っています。これは「多引数」関数に対応するためのものです:

liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c
liftA3 :: Applicative f => (a -> b -> c -> d) -> f a -> f b -> f c -> f d

実際には対応するliftM2liftM5まで)もあります:

liftM2 :: Monad m => (a1 -> a2 -> r) -> m a1 -> m a2 -> m r
liftM3 :: Monad m => (a1 -> a2 -> a3 -> r) -> m a1 -> m a2 -> m a3 -> m r

例えば:

> liftM2 (+) (Just 1) (Just 1)
Just 2
> liftM3 (\x y z -> x + y + z) (Just 1) (Just 2) (Just 3)
Just 6

fmapからliftMへ、さらにliftM5まで理解しやすくなります

join

monadic value の value がまだ monadic value である状況に直面する可能性があります。例えば:

Just (Just 1)

では、内側の monadic value をどのように取り出せばよいでしょうか?join関数を使用できます:

join :: Monad m => m (m a) -> m a

試してみましょう:

> join (Just (Just 1))
Just 1
> join Nothing
Nothing
> join (Just (Just (Just 1)))
Just (Just 1)

注意すべき点は、型上で内側と外側のMonadが同じ(どちらもm)である必要があるため、join (Just [1])などは正常に動作しないことです

上記のMaybeの例から見ると、joinには実際的な意味がないように見えますが、他のMonadを見てみましょう:

> join [[1, 2], [3], [4, 5]]
[1,2,3,4,5]
> join (writer (writer (1, "a"), "b")) :: Writer String Int
WriterT (Identity (1,"ba"))

少し join(接続)という意味が出てきました。内側の monadic value を取り出した後、何らかの演算も行っているようです。推測すると以下のようになります:

join' mm = do
  m <- mm
  m

これは以下と等価です:

join'' mm = mm >>= (\m -> m)
-- つまり
join'' mm = mm >>= id

では、内側の List は誰によって接続されるのでしょうか?List の>>=の実装は List Comprehension です:

xs >>= f             = [y | x <- xs, y <- f x]

したがって List の状況では、以下と等価です:

joinList xss = xss >>= (\xs -> xs)
joinList' xss = [y | x <- xss, y <- id x]

Writerの状況は List と類似しており、演算はすべて>>=によって完了されます。Maybeに演算がないのも、その>>=の実装によるものです:

(Just x) >>= k      = k x
Nothing  >>= _      = Nothing

join中のkidなので、内側のMaybe値をそのまま取り出すだけです

P.S.また、面白いものがあります:

m >>= f = join (fmap f m)

つまり>>=は、変換関数(f :: a -> m b)で monadic value(m)をマップし、monadic monadic value を取得し、さらにjoinで内側の monadic value を取り出すのが最終結果です:

> fmap (\x -> return (x + 1)) (Just 1) :: Maybe (Maybe Int)
Just (Just 2)
> join (Just (Just 2))
> Just 2

> Just 1 >>= (\x -> return (x + 1))
Just 2

実際には、この等価関係は>>=を実装する別の思路を提供しますjoinを実装すればよいのです。これはカスタム Monad instance を実装する際に特に有用です。>>=をどのように実装すれば Monad laws を遵守できるか分からない場合、角度を変えて、ネストされた monadic value を平らにするjoinを実装することを考えてみてください

filterM

通常のfilterに似ています:

filter :: (a -> Bool) -> [a] -> [a]

filterMはこのようになります:

filterM :: Applicative m => (a -> m Bool) -> [a] -> m [a]

注意すべき点は、predicate 関数(a -> m Bool)のBool戻り値に context が付加されていることです。これにはどのような効果があるのでしょうか?

フィルタリング処理中に context を追加でき、結果(m [a])に保持されます。例えば:

> graterThan2 = filterM (\x -> if (x > 2) then writer (True, [show x ++ " reserved"]); else writer (False, [show x ++ " discarded"])) [9, 5, 0, 2, 1, 3] :: Writer [String] [Int]
> fst . runWriter $ graterThan2
[9,5,3]
> mapM_ putStrLn (snd . runWriter $ graterThan2)
9 reserved
5 reserved
0 discarded
2 discarded
1 discarded
3 reserved

List をフィルタリングすると同時に、Writer Monadを使用して操作ログを記録しています。特に捨てられた要素も関連情報を記録しています(例えば0 discarded)。とても面白いです

さらに面白い使い方があります:

powerset :: [a] -> [[a]]
powerset = filterM (\x -> [True, False])

奇妙な関数を定義しました。配列を受け取り、二次元配列を返します。試してみましょう:

> powerset [1, 2, 3]
[[1,2,3],[1,2],[1,3],[1],[2,3],[2],[3],[]]

作用から見ると、冪集合(集合のすべての部分集合からなる集合。空集合と自身を含む)を求める関数です。filterMがどのように実現しているのか考えてみましょう?

predicate 関数\x -> [True, False]は同時に複数の結果を返します:保持と破棄。これは List のnon-deterministic 文脈の応用です:

[a] は同時に多くの結果を持つ computation を表します(non-deterministic computation)

non-deterministic 計算は複数の結果を生み出すことができるため、powersetの状況では、冪集合を求める効果的な方法は:集合内の各要素を遍历し、2 つの操作(保持と破棄)を行い、操作結果を収集することです

filterMの実装を見てみましょう:

filterM          :: (Applicative m) => (a -> m Bool) -> [a] -> m [a]
filterM p        = foldr (\ x -> liftA2 (\ flg -> if flg then (x:) else id) (p x)) (pure [])

Control.Monad から引用)

foldrは配列を遍历し、初期値はpure []、累加関数(accumulator)は\ x -> liftA2 (\ flg -> if flg then (x:) else id) (p x)です。現在の要素にliftA2でマップし、p xの結果に応じてx:またはidを返します(保持の場合、現在の要素を結果セットに挿入。破棄の場合、結果セットは元のまま)

少し分かりにくいですが、パラメータを補完すると、実際には以下のようになります:

\ x ma -> liftA2 (\ flg a -> if flg then (x: a) else id a) (p x) ma

foldr :: Foldable t => (a -> b -> b) -> b -> t a -> bは二引数関数を受け取り、パラメータの順序は現在の要素と累加結果(それぞれ上記のxmaに対応。maの初期値はpure [])です。liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f cは二引数関数を 2 つの monadic value(それぞれp xma)に適用し、さらに monadic value を返します。最後に、これらの monadic value はfoldrによってmappendで折りたたまれて最終結果が得られます

P.S.その通り、foldrの実装にはfoldMap :: Monoid m => (a -> m) -> t a -> mが使用されています。詳細はData.Foldable を参照

foldM

foldlに対応する monadic バージョンはfoldMと呼ばれます:

foldl :: Foldable t => (b -> a -> b) -> b -> t a -> b
foldM :: (Foldable t, Monad m) => (b -> a -> m b) -> b -> t a -> m b

例えば:

> foldl (\a x -> a + x) 0 [1..10]
55

P.S.小さな詳細ですが、foldlfoldrの累加関数のパラメータ順序は逆です。前者はa v、後者はv aです

foldlに計算文脈(例えば失敗する可能性のある文脈)を追加したい場合、foldMで簡単に実現できます:

> foldM (\a x -> if (a > 99) then Nothing; else Just (a + x)) 0 [1..10]
Just 55
> foldM (\a x -> if (a > 99) then Nothing; else Just (a + x)) 0 [1..100]
Nothing

この状況では、合計が99を超えると、大数オーバーフローとして計算失敗とみなされます

foldMの実装を推測してみましょう:

foldM' acc a xs = foldl (\ma v -> ma >>= (\a -> acc a v)) (return a) xs

重要な点は累加値の context を維持することで、演算に参加(accに渡す)する際に外し、遍历時に追加します。標準(ライブラリ)の答えは以下の通りです:

foldM          = foldlM
foldlM f z0 xs = foldr f' return xs z0
  where f' x k z = f z x >>= k

ちょっと待ってください、f z x >>= kでは何が起こっているのでしょうか?

f は累加関数
z0 は初期値
xs は Foldable インスタンス
z は累加値
x は現在の要素
k は?return のようで、普通値を受け取り、context を持つ値を返す

一歩ずつ見ていきましょう。その中でf'の型は:

f' :: Monad m => t -> (a -> m b) -> a -> m b

foldrの型は:

foldr :: Foldable t => (a -> b -> b) -> b -> t a -> b

最も捉えにくいのは:

(foldr f') :: (Foldable t, Monad m) => (a -> m b) -> t t1 -> a -> m b

この段階で何が起こったのでしょうか?

foldrに 1 つのパラメータだけを与える場合、二引数関数a -> b -> bが必要で、2 番目のパラメータと戻り値の型は同じである必要があります。したがって*f'を見る姿勢を変える*べきです:

f' :: Monad m => t -> (a -> m b) -> (a -> m b)

此时ba -> m bに相当し、foldrに対応します:

foldr :: Foldable t => (a -> b -> b) -> b -> t a -> b
--  (a -> b -> b) は f' によって埋められ、残り b -> t a -> b。b を a -> m b に置き換え
foldr f' :: Foldable t => (a -> m b) -> t a -> (a -> m b)
-- つまり
(foldr f') :: (Foldable t, Monad m) => (a -> m b) -> t t1 -> a -> m b

次に 3 つのパラメータを与えます。それぞれ:

return :: (a -> m b)
xs :: t t1
z0 :: a

スムーズにm bを返します

P.S.このような��妙な変換ができるのは、Haskell 関数のデフォルトのカリー化特性によるものです。パラメータを埋めて初めて値を返します。したがって:

f' :: Monad m => t -> (a -> m b) -> a -> m b
-- 等価
f' :: Monad m => t -> (a -> m b) -> (a -> m b)

理解しやすいです。f' :: Monad m => t -> (a -> m b) -> (a -> m b)は 2 つのパラメータを受け取り、a -> m bの関数を返します(以前は 3 つのパラメータを受け取り、m b値を返していました)

同様に、このようにすることもできます:

f :: (Num b, Monad m) => b -> (t -> t1 -> m b) -> t -> t1 -> m b
f a b c d = b c d >>= \v -> return (v + a)

foldr fに対応する型は:

foldr f :: (Foldable t, Num b, Monad m) => (t1 -> t2 -> m b) -> t b -> t1 -> t2 -> m b

試してみましょう:

> foldr f (\x y -> return (x + y)) [1..10] 0 0
55

P.S.標準の答えのヒントを受けて、パラメータの順序を変更し、lambda を 1 つ減らすことができます:

foldM'' acc a xs = foldr (\v ma -> ma >>= acc v) (return a) xs

合成

関数は合成できることを知っています:

(.) :: (b -> c) -> (a -> b) -> a -> c

f . g . hは右から左へ合成します。例えば:

> ((+1) . (/2) . (subtract 1)) 7
4.0

monadic function も function なので、当然合成できます(実際にはすでに見たことがあります)

Monad laws で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

(<=<)       :: Monad m => (b -> m c) -> (a -> m b) -> (a -> m c)
(<=<)       = flip (>=>)

<=<.の monadic バージョンで、作用も似ています:

> Just 7 >>= (return . (+1) <=< return . (/2) <=< return . (subtract 1))
Just 4.0

一連のMonad m => a -> m aの関数を合成するために使用され、合成順序も右から左です

コメント

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

コメントを書く