關於 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 是什麼回去把高等代數啃一遍...

不過呢, 過了這一關我仍是不會寫稍微複雜點的程序, 類型系統難點真挺多的

相關文章
相關標籤/搜索