CirruScript 写的: 函数式编程另类指南

0

这篇笔记是我在理解函数式编程过程当中的一些思考整理成的
大概也是我在学习当中遇到过的坎, 还有灵光一现的地方
代码是用 CirruScript 写的, 尽情吐槽奇怪的语法吧
因为我主要写前端, 所以这边对强类型和并行不做涉及

这篇文章里的细节, 除了不完整, 可能还会有错, 看到请指出
灵光一现也可能是脑子一热... 总之就是理的一些想法啦

函数的写法

CirruScript 是支持过程式语言的函数体的, 但是为了 FP 限制一下
这里的 \ 表示 Lambda 也就是 λ, 毕竟有点像 Haskell:

\ (x) (+ x 1)

对应的意思就是:

f(x) = x + 1

let 绑定变量

Haskell 是纯函数式编程, 不能用赋值的, 不过有 let
虽然不好赋值, 但是可以变相给数据设置一个名字, 比如说这样

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 映射, 只是映射的结果都会被合并到一起

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


如果觉得我的文章对你有用,请随意赞赏

你可能感兴趣的

载入中...