关于 Monad 的学习笔记

假期终于看明白了 Monad, 这个关卡卡了好几年了, 终于过了
我如今只能说初步了解到 Monad, 不够深刻, 打算留一点笔记下来html

如今回头看, 若是从前学习得法的话, 最快可能几天或者几周就搞定的
好比说有 Node.js 那样成熟的社区跟教程, 或者公司里有就有人教的话
此前在 Haskell 中文论坛问过, 知乎问过, 微博私信问过, 英文教程也看了
整体上 Monad 就成了愈来愈吸引我注意力的一个概念git

Rich Hichey 的影响

我强烈推荐 Rich Hickey 的演讲, 由于我以为他很是有智慧
https://github.com/matthiasn/talk-transcripts/tree/master/Hickey_Rich
虽然不少是我听不懂的, 但让我能从更高的层次去理解函数式编程为何好
好比说变量的问题, 他讲了好多例子, 讲清楚数据会发生改变是不可靠的
还有保持简单对于系统的可靠性会带来多大改善, 为何面向对象有问题
好吧大部分是我听不懂, 但感受颇有启发程序员

过程式编程是直观的, 但也是很存在问题的, 特别是学了函数式编程再回头看
好比说 null 值的问题, 看似天然而然, 实际倒是考虑不够严谨
还有语句(或者说指令)按顺序执行的问题, 也很天然, 实际却考虑不足
这类问题致使咱们在编写代码过程当中不断发现有特殊的状况须要回头考虑
诚然迎合了新人学习编程所需的方便, 可代价倒是对代码控制流的操做不够强大github

我不否定有丰富经验跟能力的程序员能用过程式代码写出极为可靠的程序
然而引入函数式编程强大的复合能力, 有可能让程序变得更加简短清晰
并且如同 Haskell 这样搭配类型系统, 能让难以理解的过程稍微变得直观一些
固然, 函数式编程所需的抽象能力真的不是为新手准备的, 这带来巨大的门槛编程

纯函数

要理解 Monad 首先要对纯函数有足够的认识, 我假设读者有了解过 Haskell
相比过程式语言当中的函数(或者叫方法, procedure), Haskell 当中有不少不一样:数组

  • Haskell 当中能定义, 但不能赋值, 不能修改已经定义好的数据
  • 纯函数传入参数相同, 返回值就必定相同, 不会有例外
  • 读写文件这类 IO 操做, 也是有返回值的, 好比 IO String, IO ()
  • Haskell 当中没有语句用于实现过程, 而是用函数模拟出来过程

最后一点跟流行编程语言区别尤为大, 即使跟 Lisp 的设计也差异很大
Lisp 虽然号称"一切皆表达式", 但在函数体, 在 begin 当中语句照样用:app

racket(define (print-back)
  (define x (read))
  (print x))

好比这样的一段 Racket, 转化成 Haskell 看起来像是这样:编程语言

haskellprintBack :: IO ()
printBack = do
  x <- getLine
  print x

然而 do 表达式并非 Haskell 真实的代码, 这是一套语法糖
执行过程会被转化为 >>= 或者 >> 函数, 就像是下面这样:ide

haskellprintBack = getLine >>= (\x -> print x)

或者把函数放到前面来, 这样看得就更明确了:函数式编程

haskellprintBack = (>>=) getLine (\x -> print x)

就是说 getLine 的执行结果, 还有后面的函数, 都是 >>= 这个函数的参数
后边的 (\x -> print x) 几乎就是个回调函数, 对, 相似 Callback Hell
因此 do 表达式彻底就是个障眼法, Haskell 里大量使用回调的写法
同时由于回调, 因此 Haskell 不会暗地里并行执行参数里的操做, 而是有明确的前后顺序
只不过 Haskell 语法灵活, 大量嵌套函数, 看起来还能跟没事同样, 看文档:
http://en.wikibooks.org/wiki/Haskell/do_notation

总结一下就是纯函数编程, 过程式语言经常使用的招数都被废掉了
整个 Haskell 的函数都往数学函数逼近, 好比 f(x) = x^2 + 2*x + 1
另外, 加上了一套代数化的类型系统, 可以容纳编程须要的各类类型

IO 的特殊性

IO 要特别梳理一下, 由于相较于过程式语言, 这里的 IO 处理很奇怪
https://wiki.haskell.org/IO_inside
一般编程语言的作法, 好比说经常使用的读取文件吧, 调用, 返回字符串, 很好理解:

jscontent = fs.readFileSync('filename', 'utf8') // Node.js
juliacontent = readall("filename") # Julia
racket(define content (file->string "filename")) ; Racket

但在纯函数语言当中有个大问题, 不是说好了参数同样, 返回值同样吗?
因此在 Haskell 当中 readFile 返回值并非 String, 而是加上了 IO:

haskellreadFile :: IO String

结果就是处理文件内容时, 必需引入 Monad 的写法才行:

haskellmain = do
  content <- readFile "filename"
  putStr content

这个地方的 IO StringString 作了一层封装, 后面会遇到更多封装

代数类型系统

关于这一点, 我理解不许确, 可是举一些例子大概能够明白一些,
好比这是相似加法的方式定义新的类型:

haskelldata MySumType = Foo Bool | Bar Char

这是相似乘法的方式定义新的类型:

haskelldata MyProductType = Baz (Bool, Char)

这是以递归的方式定义新的类型:

haskelldata List a = Nil | Cons a (List a)

相比 C 或者 Go 经过 struct 定义新的类型, Haskell 显得很数学化
由于, 若是用在 Go 里定义类型是 A 或者 B, 怎么定义? 还有递归?

Haskell 当中关于类型的概念, 整理在一块儿就是一些关键字:

  • data, type, newtype 用来定义类型或者类型的别名
  • instance, class 用来实现类型之间的关联, 或者说定义实现类型类

具体看这篇文章归纳的, Haskell 当中类型, 类型类的一些操做
http://joelburget.com/data-newtype-instance-class/

这里的概念跟面向对象方面的, "类", "接口", "继承"有不少类似之处
可是看下例子, 这在 Haskell 当中是怎样使用的,
好比有一个叫作 Functor 的 Typeclass, 不少的 Type 都属于这个 Typeclass:

haskellclass Functor f where  
    fmap :: (a -> b) -> f a -> f b

好比 Maybe Type 就是基于 Functor 实现, 首先用 data 定义 Maybe Type:

haskelldata Maybe a = Just a | Nothing
    deriving (Eq, Ord)

而后经过 instanceMaybe 上实现 Functor 约定的函数 fmap:

haskellinstance Functor Maybe where
    fmap f (Just x) = Just (f x)
    fmap f Nothing = Nothing

再好比 [] 也是, 那么首先 [] 大体能够这样定义
而后会有 [] 上实现的 Functor 约定的 fmap 方法:

haskelldata [a] = [] | a : [a] -- 演示代码, 可能有遗漏

instance Functor [] where
    fmap = map

还有一个例子好比说 Tree Type, 也能够一样实现 fmap 函数:

haskelldata Tree a = Node a [Tree a]

instance Functor Tree where
    fmap f (Leaf x) = Leaf (f x)
    fmap f (Branch left right) = Branch (fmap f left) (fmap f right)

就是说, Haskell 当中的类型, 是经过这样一套写法定义出来的
一样, Monad 也是个 Typeclass, 也就能够按上边这样理解
单看写法, Go 的 interface 定义看起来类似, 至少语法上能够理解

Functor, Applicative, Monad

Haskell 首先是咱们熟悉的 Value 还有 Function 的世界
Functor, Applicative, Monad 在大谈封装的问题,
就是值会被装进一个盒子当中, 而后从盒子外边用这三种手法去操做,
http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_...

首先难以理解的是, 这层封装是什么? 为何硬生生造出一个其余语言没有的概念?
考虑到 Haskell 当中大量的 Category Theory(范畴论)的术语, 好像高等代数学到过..
范畴论群论依然是我没法理解的数学语言, 因此这我依然不能解释, 究竟为何有一层封装?
没有办法, 只能先看一下这一层封装在 Haskell 当中派上了什么用场?

  • Maybe

首先 Maybe Type 实现了 Monad, 那么看下 Maybe 典型的场景
注意下 Haskell 里 1 / 0 结果是 Infinity,, 这个大概也不是咱们想要的
下面是封装过的除法, 0 不能做为被除数, 因此有了个 Nothing:

haskelldivide :: (Fractional a) => a -> a -> Maybe a
divide a 0 = Nothing
divide a b = Just $ a / b

考虑一下这样一个四则运算, 上面提示了, 一个状况 b 多是 0, 除法有问题
可是做为例子, 不少 x / 0 在实际的编程当中咱们会当成报错来处理,
好, 先认为报错, 那么整个程序就退出了

haskell((a / b) * c) + d

不过, 引入 Maybe Type 给出了一套不一样的方案, 对应有报错和没有报错的状况:

haskell(Just 0.5 * Just 3) + Just 4
Just 1.5 + Just 4
Just 4.5
haskell((Just 1 / Just 0) * Just 3) + Just 4
(Nothing * Just 3) + Just 4
Nothing + Just 4
Nothing

没有报错, 一切正常. 若是有报错后边的结果都是 Nothing
这个就像 Railway Oriented Programming 给的那样, 增长了一套可能的流程:
http://fsharpforfunandprofit.com/posts/recipe-part2/

  • List

而后, List 也实现了 Monad, 就来看下例子, 下面一段代码打印了什么结果

haskellexample :: [(Int, Int, Int)]
example = do
  a <- [1,2]
  b <- [10,20]
  c <- [100,200]
  return (a,b,c)
-- [(1,10,100),(1,10,200),(1,20,100),(1,20,200),(2,10,100),(2,10,200),(2,20,100),(2,20,200)]

实际上是列表解析, 若是按花哨的写法写, 应该是这样:

haskell[(a, b, c) | a <- [1,2], b <- [10,20], c <- [100,200]]
  • (->) r

后面的两个例子难以理解, 可是大概看一看, (->) r 也实现了 Functor Typeclass
(->) r 是什么? 是函数, 一个参数的函数. 注意 Haskell 里的函数参数都是一个...

haskellinstance Functor ((->) r) where
    fmap = (.)

函数做为 fmap 第二个参数, 最后效果竟然是实现了函数复合! f . g

haskellghci> :t fmap (*3) (+100)
fmap (*3) (+100) :: (Num a) => a -> a
ghci> fmap (*3) (+100) 1
303
  • sequenceA

更复杂的是实现了 Applicative Typeclass 的 sequenceA 函数

haskellsequenceA :: (Applicative f) => [f a] -> f [a]  
sequenceA = foldr (liftA2 (:)) (pure [])

这个函数能把别的函数组合在一块儿用, 还能把 IO 操做组合在一块儿用,
并且这么密集的抽象... 3 个 IO 操做被排在一块儿了...

haskellghci> sequenceA [(>4),(<10),odd] 7  
[True,True,True]  
ghci> and $ sequenceA [(>4),(<10),odd] 7  
True  

ghci> sequenceA [getLine, getLine, getLine]  
heyh  
ho  
woo  
["heyh","ho","woo"]

好, 回到上面的问题, Functor, Applicative, Monad 为何有?
以前说函数是语言一切都是函数, 一些过程式的写法写不了了,
如今借助几个抽象, 好像又回来了, 并且花样还不少.. 连复合函数都构造了一遍
在这样的认识之下, 再看下 IO Monad 作了什么, 加上 do 表达式:

haskellmain :: IO ()
main = do putStrLn "What is your name: "
          name <- getLine
          putStrLn name

彻底就是在模仿面向过程的编程, 或者说把面向过程里的一些东西从新造了一遍
固然我我的学到这里依然没明白设计思路, 但我知道是为何要设计了
按照教程上的说法, 我能够整理一下几个函数之间的关联的递进:

首先, Haskell 一般的代码能够看做是对基础类型进行操做
好比咱们有个函数 f, 有个数据 x, 经过 call 来调用:

haskellPrelude> let call f x = f x
Prelude> :t call
call :: (a -> b) -> a -> b

那么 call 的类型声明就是 (a -> b) -> a -> b

  • Functor
haskellclass Functor f where  
    fmap :: (a -> b) -> f a -> f b

接着是 Functor, 注意类型声明变成的改变, 多了一层封装:

haskell(a -> b) -> a -> b -- call
(a -> b) -> f a -> f b -- fmap
  • Applicative
haskellclass (Functor f) => Applicative f where  
    pure :: a -> f a  
    (<*>) :: f (a -> b) -> f a -> f b

到了 Applicative 呢, 又在前面加上了一层封装:

haskell(a -> b) -> a -> b -- call
(a -> b) -> f a -> f b -- fmap
f (a -> b) -> f a -> f b  -- <*>
  • Monad
haskellclass 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

到了 Monad, 参数顺序跟具体的封装又作了改进(m 写成 f 方便对比):

haskell(a -> b) -> a -> b -- call
(a -> b) -> f a -> f b -- fmap
f (a -> b) -> f a -> f b  -- (<*>)
f a -> (a -> f b) -> f b  -- (>>=)

大体上有个规律, 就是调用函数封装 f, 手段都是为了函数能超越封装使用
并且 f 会是什么? 有 Maybe [] ((->) r) IO, 还有其余不少
带来效果是什么? 有处理报错, 列表解析, 符合函数, 批量的 IO, 以及其余
Haskell 用纯函数补上了操做控制流和 IO 的功能, Monad 是其中一个手段

Monad 的写法

而后看下 Monad 去掉 do 表达式语法糖的时候怎么写, 原始的代码:
http://stackoverflow.com/q/16964732/883571

haskelldo num <- numberNode x
   nt1 <- numberTree t1
   nt2 <- numberTree t2
   return (Node num nt1 nt2)

去掉了语法糖, 是一串 >>= 函数链接在一块儿, 一层层的缩进:

haskellnumberNode x >>= \num ->
  numberTree t1 >>= \nt1 ->
    numberTree t2 >>= \nt2 ->
      return (Node num nt1 nt2)

还有一个 Applicative 的写法

haskellNode <$> numberNode x <*> numberTree t1 <*> numberTree t2

最后一个我得看老半天... 好吧, 总之, Haskell 就是提供了如此复杂的抽象
print("x") 在过程式语言中仅仅是指令, 在 Haskell 中却被处理为纯函数的调用
Haskell 将纯函数用于高阶的函数的转化以及操做, 变成很强大的控制流
前面说了, 实际上只是做为参数, 跟 Node.js 使用深度的回调很类似

不过还记得 Railway Oriented 那张图吗, 跟 Node.js 对比一下:

jsfs.readFile("filename", "utf8", function(err, content) {
  if (err) { throw err }
  console.log(content)
})

注意 err 的处理, Haskell 当中可没有写 err 而是在 >>= 内部处理掉了
并且 Haskell 也不会执行到这里就吐出返回值, 而是等所有执行完再返回
上边我用过 Callback Hell 打比方, 不过除了写法类似, 其余方面差异不小

总结

好了我不是在写 Monad 教程, 我也没全弄明白, 可是上边记录了我理解的思路:

  • 可变数据, 反作用, 种种不肯定性是编程当中混乱的来源
  • 纯函数相对于过程式代码的特殊性, 决定了它不能简单使用语句或者指令直接写程序
  • Haskell 当中的 IO 作了封装, 使之融合到纯函数当中来
  • Monad 是 Haskell 当中的 Typeclass, 因此我先不去管数学中的定义
  • 什么是封装, 为何 Haskell 中函数和数据会被封装
  • Monad 起到了怎样的做用, 怎样理解它的做用

我以前一直在想 Monad 会是数学结构当中某种强大的概念, 群论如何如何
可是回头看, 这更像是人为定义出来的方便编程语言使用的几个 Typeclass 而已
当新的数据类型被须要, 还能够本身定义, 用高阶函数玩转...
总之我没必要为了弄懂 Monad 是什么回去把高等代数啃一遍...

不过呢, 过了这一关我仍是不会写稍微复杂点的程序, 类型系统难点真挺多的

相关文章
相关标签/搜索