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

相关文章
相关标签/搜索