Function Programming Note

(鉴于函数式编程语言集中在 Web 开发,比如 Lisp,Erlang, Elixir, Clojure,故暂且把它放在 web 目录下。)

Note for 函数式编程圣经

刘欣公众号上的一篇文章,写得很有趣,对于理解函数式编程很有帮助。

函数式编程最大的特点就是不变量 (Immutable),一个变量 (实际都是常量),一旦声明且赋值后,它的值就不能再改变了。

define result = 0
result = 1 // Wrong!

define myList = [1, 2, 3]
myList[0] = 10 // Wrong!

变量不能被修改,那这逻辑还怎么写啊,解决办法是,如果要修改一个变量的值,把新值赋值给一个新的变量,然后旧的变量就抛弃了,用新的变量。

第二个特点,函数应该是纯粹的,不能用副作用,因此它:

  • 不能修改传递给函数的变量!
  • 不能修改全局变量!
  • 对于同样的输入参数,返回值总是相同的!

由于以上的特点,在非函数式编程语言中常见的的循环,在函数式编程语言中居然无法使用。

// wrong! i 的值不能被修改
for (i = 0; i < 100; i++) {
  print(i)
}

function sum(list) {
  define result = 0
  for (item in list) {
    // wrong! result 的值不能被修改
    result += item
  }
  return result
}

因此,在函数式编程语言中,要用尾递归来替代循环。为了方便操作,对于 list,一般都会有一个 head 函数,来取得 list 中的第一个值,还有一个 tail 函数,来取得 list 余下的值。

以 Elixir 为例,用 hd 函数来取得第一个元素,用 tl 函数取得余下的值。

iex(1)> hd [1, 2, 3]
1
iex(2)> tl [1, 2, 3]
[2, 3]

另外,在 Elixir 中,可以方便地使用匹配来取得 head 和 tail:

iex(1)> [h | t] = [1, 2, 3, 4]
[1, 2, 3, 4]
iex(2)> h
1
iex(3)> t
[2, 3, 4]

尾递归实现 list 求和的伪代码:

define myList = [1, 2, 3, 4]
head(myList) // 1
tail(myList) // [2, 3, 4]

define sumList(list, result) {
  if ([] == list) {
    return result
  } else {
    return sumList(tail(list), result + first(list))
  }
}
sumList(myList, 0) // 10

第三个特点,高阶函数,函数是一等公民,可以作为参数传递。

常见的高阶函数,map / reduce / filter。

define a = [1, 2, 3]
map(function(x) {return x*2}, a) // [2, 4, 6]

变量不可变的特点,使用函数式编程语言天然的不需要共享变量,没有多线程竞争加锁的情况,天生为并发而生。

其它特点:柯里化 (Currying),惰性求值 (比如说参数,如果函数体中没有用到这个参数,那么这个参数可以永远不用运算求值),宏 (Macro) ...

一般函数式编程语言还有一个特点,运算符视同函数,因此放在所有参数之前 (正统函数式语言是这样,比如 Clojure,但 Elixir 的语法就偏向传统语言)。

// 一般语言
1 + 2

// 函数式语言
(+ 1 2)

函数式编程与面向对象编程

三种不同的编程思维:

  • 面向过程 - C
  • 面向对象 - C++ / Java / C#
  • 函数式编程 - Lisp / Clojure / Schema

大部分人都是先接触面向对象编程,习惯了这种编程思维,再来学习函数式编程的时候,总会和面向对象对比,疑惑为什么不能像面向对象那样处理呢。比如在看到 Elixir 的 Enum 模块时,如下例所示:

iex> Enum.all?(["foo", "bar", "hello"], fn(s) -> String.length(s) == 3 end)
false
iex> Enum.all?(["foo", "bar", "hello"], fn(s) -> String.length(s) > 1 end)
true

我就会想,咦,为什么不能像 Ruby 那样,直接在数组对象上调用 all? 方法呢,直接在字符串对象上调用 length() 方法呢,像下面这样:

["foo", "bar", "hello"].all? { |s| s.length == 3 }

特别是求字符串的长度,居然还要用一个 String.length() 来求长度,好不方便啊。

而且一直在想,为什么还没有出现 class 的使用介绍啊...

这是因为,这是函数式编程啊,函数式编程里只有基本类型和函数,没有对象。所以 Elixir 相比 Ruby,只有 module,没有 class。

像面向对象语言的高阶函数,实际就是从纯粹的函数式编程中借鉴过去的,从而形成了面向对象 + 函数式的杂合语言,支持函数式已经是现代新式面向对象的语言的标配了,像 Swift, Kotlin。

results matching ""

    No results matching ""