CirruScript 寫的: 函數式編程另類指南

這篇筆記是我在理解函數式編程過程中的一些思考整理成的
大概也是我在學習當中遇到過的坎, 還有靈光一現的地方
代碼是用 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

List Monad

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' ]

Do Notation

過程式編程, 咱們會遇到要讀多個文件, 處理一下, 打印, 好比:

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 怎麼寫起來是過程式的, 思路相似吧

Pattern Matching

函數式編程裏, 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

相關文章
相關標籤/搜索