這篇筆記是我在理解函數式編程過程中的一些思考整理成的
大概也是我在學習當中遇到過的坎, 還有靈光一現的地方
代碼是用 CirruScript 寫的, 盡情吐槽奇怪的語法吧
由於我主要寫前端, 因此這邊對強類型和並行不作涉及前端
這篇文章裏的細節, 除了不完整, 可能還會有錯, 看到請指出
靈光一現也多是腦子一熱... 總之就是理的一些想法啦編程
CirruScript 是支持過程式語言的函數體的, 可是爲了 FP 限制一下
這裏的 \
表示 Lambda 也就是 λ
, 畢竟有點像 Haskell:數組
\ (x) (+ x 1)
對應的意思就是:異步
f(x) = x + 1
let
綁定變量Haskell 是純函數式編程, 不能用賦值的, 不過有 let
雖然很差賦值, 可是能夠變相給數據設置一個名字, 好比說這樣async
var let_ $ \ (v k) (k v) let_ (+ 1 2) $ \ (three) (console.log three)
通藉助一個函數參數, 就能綁定名字了函數式編程
函數式編程裏是不能修改數據的, 那麼 for
while
在語法上不支持的for
不是要 i++
嗎, while
不是要 i--
嗎, 就要修改數據啊
那麼循環到哪裏了總要一有地方記錄的, 結果, FP 只能保存在參數裏
好比這個例子遍歷數組, 把數組追加到另外一個數組:函數
var readList $ \ (acc list) cond (> list.length 0) readList (acc.concat (list.slice 0 1)) (list.slice 1) , acc readList (array) (array 1 2 3 4)
那麼好比 Fibonacci 數要兩個變量才能存呢, 那就多用兩個參數唄:學習
var fastFibonacciHelper $ \ (n x1 x2) cond (< n 2) x1 fastFibonacciHelper (- n 1) (+ x1 x2) x1 var fibonacci $ \ (n) fastFibonacciHelper n 1 1 fibonacci 5
JavaScript 裏沒有好的數組拼接函數, 先寫一個 concat
:設計
var concat $ \ (args) concatHelper args (array) var concatHelper $ \ (list result) cond (is list.length 0) result concatHelper (list.slice 1) result.concat (. list 0)
List Monad 須要實現 return join bind
這些接口
數組呢, return
對應數組的構造器join
就是 concat
了, 把數組合併到一塊兒bind
有點像 map
映射, 只是映射的結果都會被合併到一塊兒code
var return_ $ \ (x) (array x) var join concat var bind $ \ (x f) concat (x.map f)
執行一次 bind
就會變成這樣子:
bind (array 3 4 5 6) $ \ (x) cond (< x 5) (return_ :little) (return_ :great)
結果就會獲得:
[ 'little', 'little', 'great', 'great' ]
過程式編程, 咱們會遇到要讀多個文件, 處理一下, 打印, 好比:
var fileLog $ + ":content of file is:\n" file console.log fileLog var readme $ fs.readFileSync :README.md :utf8 var readmeLog $ + ":content of readme is:\n" readme console.log readmeLog
而後轉成函數式的寫法, 所有經過參數傳遞, callback hell 就出來了
這個例子呢徹底沒考慮 IO Monad 的代數類型, 因此看起來不可怕:
var bind $ \ (v k) (k v) bind (fs.readFileSync :demo.cirru :utf8) $ \ (file) bind (+ ":content of file is:\n" file) $ \ (fileLog) bind (console.log fileLog) $ \ (_) bind (fs.readFileSync :README.md :utf8) $ \ (readme) bind (+ ":content of readme is:\n" readme) $ \ (readmeLog) console.log readmeLog
對於 Cirru 來講, 語法樹轉一轉, 還不是輕鬆的事情
設計個 Do 表達式用來生成 CPS 各類回調各類縮進的代碼就行了
Cirru 的前綴語法很難看, 湊合看看, <-
和 let
都用前綴
下劃線 _
繼續表示返回的值被丟棄了, 最後一個參數就省寫法了:
do <- file $ fs.readFileSync :demo.cirru :utf8 let fileLog $ + ":content of file is:\n" file <- _ $ console.log fileLog <- readme $ fs.readFileSync :README.md :utf8 let readmeLog $ + ":content of readme is:\n" readme console.log readmeLog
若是有異步操做的話, 不就是用回調寫的嗎... 再加上 Do 表達式的語法
想象一下異步的 async await
怎麼寫起來是過程式的, 思路相似吧
函數式編程裏, if
後邊的 else
是要返回值的, 否則類型都不匹配
不過 else if
之類寫很容易煩, 就要搞個模式模式匹配用用
CirruScript 裏用 case
模仿的, 底層是 CoffeeScript 的 switch
:
\ (a) case true (> a 1) ":Greater than 1" (< a 1) ":Litter than 1" else ":Should be 1"
先看看面向對象編程, 每一個對象都有內部狀態, 都是數據源
那麼數據就在對象之間傳遞, 交換, 相互觸發
原理很明確, 只是對象多了, 對象之間的關係多了, 大概就管不住了
而函數式編程每一個對象內部狀態都不能修改的, 怎麼可能有多個數據源
FP 代碼, 數據都是經過複製和衍生才能日後傳遞的
因此 FP 代碼一般就會造成一個單向數據流
這個事情呢, 犧牲了代碼的簡單和便利, 甚至內存的體積
好處是數據的一致性有了保證, 很難出現多個數據不一致的狀況
最後說點有用的, 就是幾門表明性的函數式語言的中文社區
感興趣的同窗能夠過去交流交流:
論壇:
http://a.haskellcn.org/
http://clojure-china.org/
http://elixir-cn.com/
QQ:
72874436 Haskell
130107204 Clojure
249122869 Elixir
微博:
http://weibo.com/haskellcnorg
http://weibo.com/clojurechina
http://weibo.com/elixircn