Haskell学习-monad

原文地址:Haskell学习-monad

什么是Monad

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

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

-- Monad的定义
class Monad m where
    return :: 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 类型,两者不匹配怎么办? >>= 函数出马了! >>= 的类型是

IO a -> (a -> IO b) -> IO b

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

getChar >>= putChar

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

do 表示法

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

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

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

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

使用 do改写,明显更加清晰,和我们熟悉的命令式语言风格差不多。
<- 表示从monadic value中取出普通值,可以看成是拆开盒子取出所需要的值。

foo :: IO String
foo = do
    x <- getLine
    y <- getLine
    print (x ++ y)
    return "over"

do语法对应模式

do {e}             -> e
do {e; es}         -> e >> do {es}
do {let decls; es} -> let decls in do {es}
do {p <- e; es}    -> e >>= \p -> es

Monad 类型

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

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

    -- Maybe 的 Monad instance
    instance Monad Maybe where
        return x = Just x
        Nothing >>= f = Nothing
        Just x >>= f  = f x
        fail _ = Nothing
    
    -- 实例
    Just 3 >>= (\x -> Nothing >>= (\y -> Just (show x ++ y)))
    > Nothing
    
    Just 3 >>= (\x -> Just "!" >>= (\y -> Just (show x ++ y)))
    > Just "3!"

    使用 do 表示法写成这样:

    foo :: Maybe String
    foo = do
        x <- Just 3
        y <- Just "!"
        Just (show x ++ y)
  2. List
    >>= 基本上就是接受一个有 context 的值,把他喂进一个只接受普通值的函数,并回传一个具有 context 的值。[ ] 其实等价于 Nothing。

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

    --list 的 Monad instance
    instance Monad [] where
        return 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 = do
        x <- [1..50]
        guard ('7' `elem` show x)
        return x
    
    -- 对应的 list comprehension
    [ x | x <- [1..50], '7' `elem` show x ]
    > [7,17,27,37,47]
  3. Either
    Control.Monad.Error 里面有 ErrorMonad instance

    instance (Error e) => Monad (Either e) where
        return x = Right x
        Right x >>= f = f x
        Left err >>= f = Left err
        fail msg = Left (strMsg msg)
    
    Right 3 >>= \x -> return (x + 100) :: Either String Int
    > Right 103

Monad 规则

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

    return 3 >>= (\x -> Just (x+100000)) -- 和直接函数调用没有区别
  2. m >>= return == m
    这一条规则对风格也有好处:如果在一系列的action块里面,如果最后一句就是需要返回的正确结果,那么就不需要使用 return 了;和第一条规则一样,这条规律也能帮助我们简化代码。

     Just "move on up" >>= return -- 可以不需要 return
  3. (m >>= f) >>= g == m >>= (\x -> f x >>= g)
    当我们用 >>= 把一串 monadic function 串在一起,他们的先后顺序不应该影响结果。
    而这不就是结合律吗?我们可以把那些子action提取出来组合成一个新action。
    (<=<) 可以用来合成两个 monadic functions, 类似于普通函数结合(.), 而(>=>) 表示结合顺序相反。

    (<=<) :: (Monad m) => (b -> m c) -> (a -> m b) -> (a -> m c)
    f <=< g = (\x -> g x >>= f)
    
    -- 普通函数结合(.)
    let f = (+1) . (*100)
    f 4
    > 401
    
    -- 合成monadic functions (<=<)
    let g = (\x -> return (x+1)) <=< (\x -> return (x*100))
    Just 4 >>= g
    > Just 401
    
    -- 也可以将 monadic 函数用foldr,id 和(.)合成
    let f = foldr (.) id [(+1),(*100),(+1)]
    f 1
    > 201

Monad 的 (->) r 形态

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

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

instance Monad ((->) r) where
    return x = \_ -> x
    h >>= f = \w -> f (h w) w

Monad 辅助函数

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

>>= :: m a -> (a -> m b) -> m b
=<< :: (a -> m b) -> m a -> m b
form :: 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 -> m Bool) -> [a] -> m [a]
foldM :: (b -> a -> m b) -> b -> t a -> m b
sequence :: t (m a) -> m (t a)
sequence_ :: t (m a) -> m ()
liftM :: (a1 -> r) -> m a1 -> m r
when :: Bool -> f () -> f ()
join :: m (m a) -> m a

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

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

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

    其实可以写成

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

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

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

    mapM print [1,2,3]
    1
    2
    3
    [(),(),()]
    
    mapM_ print [1,2,3]
    1
    2
    3

    formform_mapMmapM_ 类似,不过只是把列表参数提前。

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

  1. liftM
    liftMfmap 等价, 也有 liftM3liftM4liftM5

    liftM :: (Monad m) => (a -> b) -> m a -> m b
    liftM f m = m >>= (\x -> return (f x))
    
    liftM (*2) [1,2]
    > [2,4]
  2. ap
    ap 基本上就是 **<*>**,只是他限制在 Monad 上而不是 Applicative 上。

    ap :: (Monad m) => m (a -> b) -> m a -> m b
    ap mf m = do
        f <- mf
        x <- m
        return (f x)
    
    ap [(*2)] [1,2,3]
    > [2,4,6]
  3. join
    m >>= f 永远等价于 join (fmap f m) 这性质非常有用

    join :: (Monad m) => m (m a) -> m a
    
    join (Just (Just 9))
    > Just 9
    
    join [[1,2,3],[4,5,6]]  -- 对于 list 而言 join 不过就是 concat
    > [1,2,3,4,5,6]
  4. filterM
    filterM,除了能做 filter 的动作,同时还能保有 monadic context。

    filterM :: (Monad m) => (a -> m Bool) -> [a] -> m [a]
    
    filterM (\x -> return (x > 2)) [1,2,3,4]
    > [3,4]
  5. foldM
    foldl 的 monadic 的版本叫做 foldM

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

原文地址:https://www.cnblogs.com/edwardloveyou/p/9485614.html

时间: 2024-10-21 02:24:09

Haskell学习-monad的相关文章

Haskell学习笔记二:自定义类型

内容提要: 代数数据类型 - Algebraic Data Types: 自定义数据类型 - data关键字:值构造器:类型变量与类型构造器: 记录(Record)语法 - 简化自定义数据类型的一种语法糖: 一个完整的例子 - PurchaseOrder定义和简单计算.单元测试: 代数数据类型(Algebraic Data Types) 为什么Haskell的数据类型会有代数数据类型这个名字?回想我们初中时代,初次学习代数的情况,印象最深刻就是x,y,z代替了具体的数字,引入方程式的概念,对 解

Haskell学习笔记一:类型和类型类相关内容

内容提要: 静态类型系统: 编译时确定类型错误: 类型推导机制: 基础类型:Int,Integer,Float,Double,Bool,Char: 类型变量: 基础类型类:Eq,Ord,Show,Read,Enum,Bounded,Num,Integral,Floating: Haskell是一门函数式编程语言,被称为最为纯粹的函数式编程语言.Haskell的类型系统非常强大,其中包含了很多有趣.抽象.某种程度上充满学术气息的特质. Haskell属于静态类型语言,这意味着: 每个值或者表达式,

Real World Haskell学习笔记02

数据结构 List 像是C里的数组,只能存相同类型的数据 两个基本操作++和:,++用于连接两个list当然两个list的值必须类型相同,:是cons,用于构造列表,第一个元素必须是值,不能是列表 字符串 可以看到上下结果完全相同,"表示字符,""表示字符串,和C用指针数组表示字符串一样,haskell的字符串是字符的list,所以list的所有操作都可以用在字符串上 类型 haskell会进行类推导 :set +t可以显示输出结果的类型,使用:unset +t可以关闭输出类

Real World Haskell学习篇-第1章: 入门

1. 初识解释器ghci 1.1  查看帮助: :? 1.2  修改提示符: :set prompt ghci>>> 1.3  加自己指定模块: :module + Data.Ratio 2. 基本交互 2.1 基本算术运算 中缀表达式: 1 ghci>>> 3 ^ 3 2 27 3 ghci>>> 2 + 4 4 6 5 ghci>>> 5 / 3 6 1.6666666666666667 前缀表达式: 1 ghci>>

Real World Haskell学习笔记03

类型系统 强类型 我的理解就是类型越强,使用方法越接近数学的使用方法,也就越安全 静态类型 编译器在编译器知道值的类型,也就意味着不会出现runtime异常 自动推导 函数调用 形式为函数名 参数1 参数2 ... 函数调用的优先级比一般的操作符要高,但是比括号低 符合数据类型->元组 和list不同的是tuple可以放不同类型的数据,但是长度固定 基本操作 列表操作 head获取第一个元素,tail获取第一个元素以外的元素 take从头获取制定个数的元素,drop怎是删除 元祖操作 fst,s

Monad in Scala

Scala有很强的类型系统.加上一些隐式规则,我们可以在scala里模拟haskell的monad. 先从haskell的monad type class开始: class Monad M where ret :: a -> M a bind :: M a -> (b -> M b) -> M b 这里M是type class的参数.它是一个高阶类型, kind是 * –> *.认识到这个很重要,因为如果我们想在scala里面模拟,我们首先需要知道是否scala提供了相应的东

# 对Haskell这门语言的基本认识

Haskell语言的核心特征: 1. 函数式,而且是纯函数式(purely functional) 首先,引用一下维基百科上对“典型的函数式编程语言”的划分: 一: 纯函数式 1. 强静态类型: Miranda , Haskell 2. 弱类型: Lazy K 二:  非纯函数式 1. 强静态类型:  ML家族(包括OCaml , F#), Scala 2. 强动态类型:Lisp家族(包括Comon Lisp,Scheme, Clojure), Erlang 3. 弱类型:Unlambda 考虑

从Java和JavaScript来学习Haskell和Groovy

直击现场 记得刚接触计算机的时候,我就受到了两个非常巨大的错误观念的影响,这个观念最初是来自于老师的传授还是学长的教诲已经记不清了,但是直到我工作几年以后,才慢慢有了实际的体会: 学习和使用什么编程语言不重要,重要的是算法和设计: 程序员学习的精髓是面向对象的设计模式,掌握以后,一通百通. 简直就是是胡扯啊.也许在某个极其狭隘的上下文中还能这样说,但是泛泛而谈,这样的态度无疑是误人子弟的. 就说第一条,编程语言不但重要,而且太重要了.换句话说,学习一门新的编程语言,可能学习的是背后的范型和思考问

Haskell语言学习笔记(64)Lens(4)

Prisms data NewTask = SimpleTask String | HarderTask String Int | CompoundTask String [NewTask] deriving (Show) makePrisms ''NewTask *Main> a ^? _SimpleTask Just "Clean" *Main> b ^? _HarderTask Just ("Clean Kitchen",15) *Main>