前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Monadic Function_Haskell笔记12

Monadic Function_Haskell笔记12

作者头像
ayqy贾杰
发布2019-06-12 14:46:37
8930
发布2019-06-12 14:46:37
举报
文章被收录于专栏:黯羽轻扬黯羽轻扬

liftM

代码语言:javascript
复制
liftM :: Monad m => (a1 -> r) -> m a1 -> m r

从类型声明来看,这不就是Functorfmap嘛:

代码语言:javascript
复制
fmap :: Functor f => (a -> b) -> f a -> f b

只是把context换成了Monad而已,此外没什么区别。并且对于遵守Functor laws和Monad laws的类型,这两个函数是完全等价的,例如:

代码语言:javascript
复制
> liftM (+1) (Just 1)
Just 2
> fmap (+1) (Just 1)
Just 2

liftM的具体实现如下:

代码语言:javascript
复制
liftM   :: (Monad m) => (a1 -> r) -> m a1 -> m r
liftM f m1              = do { x1 <- m1; return (f x1) }

等价于:

代码语言:javascript
复制
liftM' f m = m >>= \x -> return (f x)

注意,这个实现并不依赖Functor的特性,仅靠Monad具有的行为就可以完成(并且如果遵守Monad laws的话,就与fmap完全等价,仅将函数应用到具有context的值上,不做任何多余的事情),从这个角度看,MonadFunctor更强大

已经证明了MonadFunctor强,那Applicative呢?

Applicative最关键的是这个东西:

代码语言:javascript
复制
(<*>) :: Applicative f => f (a -> b) -> f a -> f b

实际上用Monad也能实现,叫做ap

代码语言:javascript
复制
ap :: Monad m => m (a -> b) -> m a -> m b

很容易搞定:

代码语言:javascript
复制
mf `ap'` m = do
 f <- mf
 x <- m
 return (f x)

所以,Monad还比Applicative强大。更进一步的,如果要实现自定义Monad,可以先实现return>>=,然后就很容易实现Applicative(令<*> = appure = return)和Functor(令fmap = liftM

我们知道有liftA2(直到liftA3),用来应对“多参”函数:

代码语言:javascript
复制
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):

代码语言:javascript
复制
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

例如:

代码语言:javascript
复制
> 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的场景,例如:

代码语言:javascript
复制
Just (Just 1)

那么如何取出内层的monadic value呢?可以用join函数:

代码语言:javascript
复制
join :: Monad m => m (m a) -> m a

试玩一下:

代码语言:javascript
复制
> 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

代码语言:javascript
复制
> 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之后似乎还做了运算。猜测是这样:

代码语言:javascript
复制
join' mm = do
 m <- mm
 m

等价于:

代码语言:javascript
复制
join'' mm = mm >>= (\m -> m)
-- 即
join'' mm = mm >>= id

那内层List是被谁连接起来的呢?因为List的>>=实现是List Comprehension:

代码语言:javascript
复制
xs >>= f             = [y | x <- xs, y <- f x]

所以在List的场景,等价于:

代码语言:javascript
复制
joinList xss = xss >>= (\xs -> xs)
joinList' xss = [y | x <- xss, y <- id x]

Writer的场景与List类似,运算都是由>>=完成的,而Maybe不带运算也是因为其>>=实现:

代码语言:javascript
复制
(Just x) >>= k      = k x
Nothing  >>= _      = Nothing

join中的k就是id,所以仅原样取出内层Maybe

P.S.另外,一个有趣的东西

代码语言:javascript
复制
m >>= f = join (fmap f m)

也就是说>>=等价于用转换函数(f :: a -> m b)对monadic value(m)做映射,得到一个monadic monadic value,再通过join取出内层monadic value就是最终结果:

代码语言:javascript
复制
> 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

代码语言:javascript
复制
filter :: (a -> Bool) -> [a] -> [a]

filterM长这样:

代码语言:javascript
复制
filterM :: Applicative m => (a -> m Bool) -> [a] -> m [a]

注意,predicate函数(a -> m Bool)的Bool返回值具有context了,这有什么作用?

允许在过滤过程中加入context,并且会被保留到结果(m [a])中。例如:

代码语言:javascript
复制
> 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),很有意思

还有更有趣的用法

代码语言:javascript
复制
powerset :: [a] -> [[a]]
powerset = filterM (\x -> [True, False])

定义了一个奇怪的函数,接受一个数组,返回一个二维数组,试玩一下:

代码语言:javascript
复制
> 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场景而言,求幂集的一种有效方式是:遍历集合中的每个元素,进行两种操作(保留它和丢掉它),并把操作结果收集起来

再看filterM的实现:

代码语言:javascript
复制
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(保留的话,把当前元素插入到结果集;丢掉的话,结果集保持原状)

看起来比较迷惑,补全参数后,实际上是这样:

代码语言:javascript
复制
\ 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接受一个二元函数,其参数顺序是当前元素和累加结果(分别对应上面的xmama的初始值是pure []),liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c能够把一个二元函数应用到两个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

代码语言:javascript
复制
foldl :: Foldable t => (b -> a -> b) -> b -> t a -> b
foldM :: (Foldable t, Monad m) => (b -> a -> m b) -> b -> t a -> m b

例如:

代码语言:javascript
复制
> foldl (\a x -> a + x) 0 [1..10]
55

P.S.一个小细节,foldlfoldr的累加函数的参数顺序是相反的,前者是a v,后者是v a

如果希望给foldl添上一个计算语境(比如可能会失败的语境),用foldM能够轻松搞定:

代码语言:javascript
复制
> 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的实现:

代码语言:javascript
复制
foldM' acc a xs = foldl (\ma v -> ma >>= (\a -> acc a v)) (return a) xs

关键点在于维护累加值的context,参与运算(喂给acc)时去掉,遍历时添上。标准(库)答案是这样的:

代码语言:javascript
复制
foldM          = foldlM
foldlM f z0 xs = foldr f' return xs z0
 where f' x k z = f z x >>= k

等等,f z x >>= k发生了什么?

代码语言:javascript
复制
f是累加函数
z0是初始值
xs是Foldable实例
z是累加值
x是当前元素
k是?像是return,接受普通值,返回具有context的值

一步步看,其中f'的类型是:

代码语言:javascript
复制
f' :: Monad m => t -> (a -> m b) -> a -> m b

foldr的类型是:

代码语言:javascript
复制
foldr :: Foldable t => (a -> b -> b) -> b -> t a -> b

最难以捉摸的是:

代码语言:javascript
复制
(foldr f') :: (Foldable t, Monad m) => (a -> m b) -> t t1 -> a -> m b

这一步发生了什么?

如果只喂给foldr一个参数,要求是个二元函数a -> b -> b,要求第二个参数和返回值类型相同,所以应该换个姿势看f'

代码语言:javascript
复制
f' :: Monad m => t -> (a -> m b) -> (a -> m b)

此时b相当于a -> m b,对应到foldr中:

代码语言:javascript
复制
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个参数,分别是:

代码语言:javascript
复制
return :: (a -> m b)
xs :: t t1
z0 :: a

顺利返回m b

P.S.之所以能进行这样巧妙的变换,是因为Haskell函数默认的柯里化特性,只有填满参数,才返回值。所以:

代码语言:javascript
复制
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)接受两个参数,返回一个a -> m b的函数(之前是接受3个参数,返回一个m b值)

类似的,可以这样做:

代码语言:javascript
复制
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对应的类型为:

代码语言:javascript
复制
foldr f :: (Foldable t, Num b, Monad m) => (t1 -> t2 -> m b) -> t b -> t1 -> t2 -> m b

试玩:

代码语言:javascript
复制
> foldr f (\x y -> return (x + y)) [1..10] 0 0
55

P.S.受标准答案的启发,可以换一下参数顺序,减少一层lambda:

代码语言:javascript
复制
foldM'' acc a xs = foldr (\v ma -> ma >>= acc v) (return a) xs

组合

我们知道函数可以组合:

代码语言:javascript
复制
(.) :: (b -> c) -> (a -> b) -> a -> c

f . g . h就是从右向左组合,例如:

代码语言:javascript
复制
> ((+1) . (/2) . (subtract 1)) 7
4.0

monadic function也是function,自然也能组合(实际上之前已经见过了)

在Monad laws中有提到过一个东西,叫做Kleisli composition

代码语言:javascript
复制
-- | 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版本,作用也类似:

代码语言:javascript
复制
> Just 7 >>= (return . (+1) <=< return . (/2) <=< return . (subtract 1))
Just 4.0

用来组合一系列Monad m => a -> m a的函数,组合顺序也是从右向左

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-07-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端向后 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • liftM
  • join
  • filterM
  • foldM
  • 组合
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档