猿学-Haskell学习-monad

什么是Monad

Haskell是一门纯函数式的语言,纯函数的优点是安全可靠。函数输出完全取决于输入,不存在任何隐式依赖,它的存在如同数学公式般完美无缺。可是纯函数因为隔绝了外部环境,连最基本的输入输出都无法完成。而 Monad 就是 Haskell 给出的解决方案。但Monad 并不仅仅是 IO 操作的抽象,它更是多种类似操作之间共性的抽象。所以 Monad 解决的问题并不局限在 IO 上,像 Haskell 中的 Maybe 和 [] 都是 Monad。Haskell 中漂亮的错误处理方式, do 表示法和灵活的列表推导式 (list comprehension) 都算是 Monad 的贡献。

Monad 基本上是一种加强版的 Applicative Functor,正如 Applicative Functor 是 Functor 的加强版一样。所以在充分理解 Applicative Functor 的基础上,过渡到 Monad 其实是非常平滑的。

-- Monad的定义classMonadmwherereturn :: a -> m a    (>>=) :: m a -> (a -> m b) -> m b    (>>) :: m a -> m b -> m b    x >> y = x >>= \_ -> y    fail ::String-> m a    fail msg = error msg

return 跟其他语言中的 return 是完全不一样的,它是一个把普通值包进一个 context 里面的函数,并不是结束函数执行的关键字。其实等价于Applicative中的 pure

>> 忽略前面表达式的返回值,直接执行当前表达式。

>>= 接受一个 monadic value(也就是具有 context 的值,可以用装有普通值的盒子来比喻)并且把它喂给一个接受普通值的函数,并回传一个 monadic value。

=<< 和上面 >>= 功能一样,只是结合顺序相反。

Monad 的原理

函数之间要协作,就必须以各种形式交互连接。但如何隔离纯函数与副作用函数,同时又能让两类函数相互复用呢?

以 IO 操作为例子分析,为了充分隔离纯函数与 IO 函数,Haskell 中不能实现 IO Char -> Char 这样一种输入是 IO 类型返回值却是普通类型的函数。否则副作用函数就能很容易变身为纯函数了。事实上一旦参数中有 IO,返回值必有 IO,这就保证了充分隔离。

那如何让纯函数与 IO 函数相互复用呢?这就要靠 IO Monad 中定义的 return 和 >>= 这两个函数了。return (在 Haskell 中不是关键字,只是一个函数名)的作用是将某个类型为 A 的值 a 提升(装箱)为类型为 IO A 的值 Char -> IO Char 。有了这个函数后,纯函数就可以通过 return 变成返回值为 IO 带副作用的函数了。

有了提升而没有下降操作,怎么复合 putChar :: Char -> IO() 与 getChar :: IO Char 呢。 getChar 从 IO 读取一个字符, putChar 把字符写入 IO。但 getChar 返回的是 IO Char 类型,而 putChar 需要的是普通的 Char 类型,两者不匹配怎么办? >>= 函数出马了! >>= 的类型是

IOa -> (a ->IOb) ->IOb

这样 >>= 就可以连接 getChar 与 putChar ,把输入写到输出中

getChar>>= putChar

可以看到 >>= 操作实际上是类型下降(或拆箱)操作,同时执行下降操作的函数返回值也必须是 IO 类型。这样既充分隔离纯函数与副作用函数,又能让函数相互复用。通过 return 和 >>= 两个平行世界 (范畴) 就有了可控的交流通道。

do 表示法

Haskell的 do 表示法实际上是Monad的语法糖:它给我们提供了一种不使用 (>>=) 和匿名函数来写monadic代码的方式。去除do语法糖的过程就是把它转换为 (>>=) 和匿名函数。

do 表示法可以使用分号 ; 和大括号 { } 将语句分块;但一般会使用一个表达式一行的方式,不同的作用域用不同的缩进区分。

我们还是以IO 为例子,接受两次的键盘输入,然后将两次输入的字符串合并成一个字符串,最后屏幕打印输出。 >>= 会接受前面表达式的值;>>则会忽略前面表达式的值;这里使用 return 实际它返回的仍然是IO String,因为Haskell会自动类型推导得出。monadic 的表达式代码如下:

(++) <$> getLine <*> getLine >>= print >> return"over"111222>"111222">"over"

使用 do改写,明显更加清晰,和我们熟悉的命令式语言风格差不多。

<- 表示从monadic value中取出普通值,可以看成是拆开盒子取出所需要的值。

foo::IOStringfoo=dox <- getLine    y <- getLine    print (x ++ y)    return"over"

do语法对应模式

do{e}            -> edo{e; es}        -> e >>do{es}do{letdecls; es} ->letdeclsindo{es}do{p <- e; es}    -> e >>= \p -> es

Monad 类型

来看一下几个默认的Monad类型,它们都必须实现 return,>>=,fail这几个函数。

Maybe

中间任何一步只要有Nothing,结果就提前返回Nothing。没有任何意外的情况才返回Just 值

-- Maybe 的 Monad instanceinstanceMonadMaybewherereturn x =JustxNothing>>= f =NothingJustx >>= f  = f x    fail _ =Nothing-- 实例Just3>>= (\x ->Nothing>>= (\y ->Just(show x ++ y)))>NothingJust3>>= (\x ->Just"!">>= (\y ->Just(show x ++ y)))>Just"3!"

使用 do 表示法写成这样:

foo::MaybeStringfoo=dox <-Just3y <-Just"!"Just(show x ++ y)

List

>>= 基本上就是接受一个有 context 的值,把他喂进一个只接受普通值的函数,并回传一个具有 context 的值。[ ] 其实等价于 Nothing。

当我们用 >>= 把一个 list 喂给这个函数,lambda 会映射每个元素,会计算出一串包含一堆 list 的 list,最后再把这些 list 压扁,得到一层的 list。这就是我们得到 列表 list 处理 Mondic value 的过程。

--list 的 Monad instanceinstanceMonad[]wherereturn x = [x]    xs >>= f = concat (map f xs)    fail _ = []-- 实例[3,4,5] >>= \x -> [x,-x]> [3,-3,4,-4,5,-5][1,2,3] >>= \x -> return (-x)> [-1,-2,-3]

list comprehension 也不过是 Monad 的语法糖

[1,2] >>= \n -> ['a','b'] >>= \ch -> return (n,ch)-- Monad[ (n,ch) | n <- [1,2], ch <- ['a','b'] ]-- list comprehension> [(1,'a'),(1,'b'),(2,'a'),(2,'b')]

list comprehension 的过滤基本上跟 guard 是一致的。

[1..50] >>= (\x -> guard ('7' `elem` show x) >> return x)> [7,17,27,37,47]

用 do 改写, 如果不写最后一行 return x,那整个 list 就会是包含一堆空 tuple 的 list。

sevensOnly:: [Int]sevensOnly=dox <- [1..50]    guard ('7' `elem` show x)    return x-- 对应的 list comprehension[ x | x <- [1..50], '7' `elem` show x ]> [7,17,27,37,47]

Either

在 Control.Monad.Error 里面有 Error的 Monad instance

instance(Errore) =>Monad(Eithere)wherereturn x =RightxRightx >>= f = f xLefterr >>= f =Lefterr    fail msg =Left(strMsg msg)Right3>>= \x -> return (x +100) ::EitherStringInt>Right103

Monad 规则

return a >>= f == f a

== 左边的表达式等价于右边的表达式。如果仅仅是把一个值包装到monad里面然后使用 (>>=) 调用的话,我们就没有必要使用 return ;这条规则对于我们的代码风格有着实际的指导意义:我们不应该写一些不必要的代码;这条规则保证了简短的写法和冗余的写法是等价的。

return3>>= (\x ->Just(x+100000))-- 和直接函数调用没有区别

m >>= return == m

这一条规则对风格也有好处:如果在一系列的action块里面,如果最后一句就是需要返回的正确结果,那么就不需要使用 return 了;和第一条规则一样,这条规律也能帮助我们简化代码。

Just"move on up">>= return-- 可以不需要 return

(m >>= f) >>= g == m >>= (\x -> f x >>= g)

当我们用 >>= 把一串 monadic function 串在一起,他们的先后顺序不应该影响结果。

而这不就是结合律吗?我们可以把那些子action提取出来组合成一个新action。

(<=<) 可以用来合成两个 monadic functions, 类似于普通函数结合(.), 而(>=>) 表示结合顺序相反。

(<=<) :: (Monadm) => (b -> m c) -> (a -> m b) -> (a -> m c)f<=< g = (\x -> g x >>= f)-- 普通函数结合(.)letf = (+1) . (*100)f4>401-- 合成monadic functions (<=<)letg = (\x -> return (x+1)) <=< (\x -> return (x*100))Just4>>= g>Just401-- 也可以将 monadic 函数用foldr,id 和(.)合成 letf = foldr (.) id [(+1),(*100),(+1)]f1>201

Monad 的 (->) r 形态

(->) r 不只是一个 functor 和 applicative functor,同时也是一个 monad

每一个 monad 都是个 applicative functor,而每一个 applicative functor也都是一个 functor。尽管 moand 有 functor 跟 applicative functor的性质,但他们不见得有 Functor 跟 Applicative 的 instance 定义。

instanceMonad((->) r)wherereturn x = \_ -> x    h >>= f = \w -> f (h w) w

Monad 辅助函数

带下划线函数等价于不带下划线的函数, 只是不返回值

>>= :: m a -> (a -> m b) -> m b=<< :: (a -> m b) -> m a -> m bform:: t a -> (a -> m b) -> m (t b)form_:: t a -> (a -> m b) -> m ()mapM:: (a -> m b) -> t a -> m (t b)mapM_:: (a -> m b) -> t a -> m ()filterM:: (a -> mBool) -> [a] -> m [a]foldM:: (b -> a -> m b) -> b -> t a -> m bsequence:: t (m a) -> m (t a)sequence_:: t (m a) -> m ()liftM:: (a1 -> r) -> m a1 -> m rwhen::Bool-> f () -> f ()join:: m (m a) -> m a

其中在 IO 中经常用到的一些函数

sequence

sequence 接受一串 I/O action,并回传一个会依序执行他们的 I/O action。运算的结果是包在一个 I/O action 的一连串 I/O action 的运算结果。

main=doa <- getLine    b <- getLine    c <- getLine    print [a,b,c]

其实可以写成

main=dors <- sequence [getLine, getLine, getLine]    print rs

一个常见的使用方式是我们将 print 或 putStrLn 之类的函数 map 到串列上。

sequence(map print [1,2,3,4,5])12345[(),(),(),(),()]

mapM 跟 mapM_

由于对一个串列 map 一个回传 I/O action 的函数,然后再 sequence 这个动作太常用了。所以函式库中提供了 mapM 跟 mapM_mapM接受一个函数跟一个串列,将对串列用函数 map 然后 sequence 结果。mapM_ 也作同样的事,只是他把运算的结果丢掉而已。在我们不关心 I/O action 结果的情况下,mapM_ 是最常被使用的。

mapMprint [1,2,3]123[(),(),()]mapM_print [1,2,3]123

form 和 form_ 与 mapM 和 mapM_ 类似,不过只是把列表参数提前。

还有一些是在 monad 中定义,且等价于 functor 或 applicative functor 中所具有的函数。

liftM

liftM 跟 fmap 等价, 也有 liftM3liftM4 跟 liftM5

liftM:: (Monadm) => (a -> b) -> m a -> m bliftMf m = m >>= (\x -> return (f x))liftM(*2) [1,2]> [2,4]

ap

ap 基本上就是 **<*>**,只是他限制在 Monad 上而不是 Applicative 上。

ap:: (Monadm) => m (a -> b) -> m a -> m bapmf m =dof <- mf    x <- m    return (f x)ap[(*2)] [1,2,3]> [2,4,6]

join

m >>= f 永远等价于 join (fmap f m) 这性质非常有用

join:: (Monadm) => m (m a) -> m ajoin(Just(Just9))>Just9join[[1,2,3],[4,5,6]]-- 对于 list 而言 join 不过就是 concat> [1,2,3,4,5,6]

filterM

filterM,除了能做 filter 的动作,同时还能保有 monadic context。

filterM:: (Monadm) => (a -> mBool) -> [a] -> m [a]filterM(\x -> return (x >2)) [1,2,3,4]> [3,4]

foldM

foldl 的 monadic 的版本叫做 foldM

foldM:: (Monadm) => (a -> b -> m a) -> a -> [b] -> m afoldM(\x y -> return (x + y))0[1,2,3]>6

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • 原文地址:Haskell学习-monad 什么是Monad Haskell是一门纯函数式的语言,纯函数的优点是安全...
    jeffzhong阅读 3,663评论 0 2
  • 1.Functor, Applicative, 和Monad,都是deal with有context的值的类型(t...
    tigerhy1阅读 388评论 0 1
  • 在C语言中,五种基本数据类型存储空间长度的排列顺序是: A)char B)char=int<=float C)ch...
    夏天再来阅读 3,074评论 0 2
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • 我们每天度过的称之为日常的生活,其实是一个个奇迹的连续也说不定。 ——《日常》 关于假期清单,尝试做了几份,最终都...
    Vicky赵ING阅读 213评论 0 1