liftM
liftM :: Monad m => (a1 -> r) -> m a1 -> m r
型宣言から見ると、これはFunctorのfmapそのものです:
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 を持つ値に関数を適用するだけで、余計なことは何もしません)。この観点から見ると、MonadはFunctorよりも強力です
MonadがFunctorより強力であることは証明済みですが、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)
したがって、MonadはApplicativeよりも強力です。さらに言えば、カスタムMonadを実装する場合、まずreturnと>>=を実装すれば、簡単にApplicative(<*> = ap、pure = return)とFunctor(fmap = liftM)を実装できます
liftA2(liftA3まで)があることは知っています。これは「多引数」関数に対応するためのものです:
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
実際には対応するliftM2(liftM5まで)もあります:
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中のkはidなので、内側の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は二引数関数を受け取り、パラメータの順序は現在の要素と累加結果(それぞれ上記のxとmaに対応。maの初期値はpure [])です。liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f cは二引数関数を 2 つの monadic value(それぞれp xとma)に適用し、さらに 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.小さな詳細ですが、foldlとfoldrの累加関数のパラメータ順序は逆です。前者は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)
此时bはa -> 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の関数を合成するために使用され、合成順序も右から左です
コメントはまだありません