[comment]: # 函數式編程 : 一個程序猿進化的故事
阿袁工作的第1天: 函數式編程的歷史
阿袁中午和阿靜一起吃午餐。阿袁說起他最近看的《艾倫·圖靈傳 如謎的解謎者》。
由於阿袁最近在學習Scala,所以關注了一下圖靈傳中關於函數式編程的一些歷史。
關於函數式編程的故事,可以從1928年開始講起:希爾伯特在當年的一個大會上,提出了他的問題:
- 第一,數學是完備的嗎?
是不是每個命題都能證明或證偽。 - 第二,數學是相容的嗎?
永遠不會推出矛盾的命題? - 第三,可判定性問題:數學是可判定的嗎?
是否存在一個算法,可以應用於任何命題,然后自動給出該命題的真假?
希爾伯特的哲學企圖是:每個問題的答案都將會是“是”。我想這個信念來自於對數學的神聖信仰。
不幸的是,在這同一個大會上,第一個問題就被否定了。一個年輕的捷克數學家,柯特·哥德爾的宣布,能夠證明,
算術一定是不完備的:存在既不能證明,也不能證偽的命題。
注:歐幾里得幾何的五大公理並不是一個反例。歐幾里得幾何可以被一階公理化為一個完備的系統。
(這句話啥意思?)我的理解是:公理是一個定義,或者說是不證自明的。
隨后,哥德爾不完備定理的第二定理又否定了第二個命題:“數學是相容的嗎?”
對於第三問題(可判定性問題),在1936年,丘奇(Alonzo Church)和艾倫·圖靈分別證明了存在不可解的問題。
圖靈提出的圖靈機模型,而丘奇提出了一個基於lambda演算(lambda calculus)的模型,這兩個模型被圖靈證明是等價的。
圖靈在圖靈機的思想上,繼續思考,逐步設計出早期的計算機(一個英國版的計算機,比馮諾依曼的計算機更早被建造出來。馮諾依曼對圖靈機也是認可的。),並且考慮人工智能的問題。
而lambda演算概念,則被發揚光大成了函數式編程思想。
偉大的數學!
函數式編程是基於表達式(expression)的一種編程范式。
函數式編程將計算視為對數學函數的求值過程。
-
函數式編程是:
- 聲明式編程(declarative programming),其含義是基於表達式(expression)。
- 基於表達式的含義:表達式是用來求值的。
- 傾向避免使用可變數據。
-
面向對象編程的特點:
- 命令式編程(imperative programming),其含義是基於陳述(statement)。
- 基於陳述的含義:語句是用來執行的。
阿袁工作的第2天: 函數式編程:告別對象,迎接函數
阿袁和阿靜中午又在一起,繼續討論函數式編程。
“我認為,我們可以把函數式編程理解成在做數學計算。這種編程風格是一種面向表達式(expression-oriented)風格。”,阿靜慢慢地說道。
"我也是這么想的。所以,作為一個面向對象的程序員,我們先要把對象的概念舍去掉。"
“是啊,倒空一些,才能學習到新的知識。”
“我們怎么考慮class的作用呢?”
“在面向對象中,class的一個主要作用的封裝。”
“那么,在函數式編程中,class的作用應該是對算法(函數)的分類了。”
“正解!我們做一個游戲,看看如果把一個面向對象的程序,變成面向表達式的程序。”
“好啊,我先用Scala寫一個面向對象的例子。”
// 這個例子的主要功能是對一個List排序。
// 這是一個基於面向對象思想的實現。
object Main {
// 一個支持排序的class。
// 這個class,需要外部提供一個比較器。
class ListSorter[T](a: List[T]) {
def data: List[T] = a
def sort(comparer: IComparer[T]): List[T] = {
return data.sortWith(comparer.compare)
}
}
// 我們為比較器定義一個interface,帶一個比較函數compare。
trait IComparer[T]{
def compare(a: T, b: T): Boolean
}
// 這個一個具體的比較器,實現了比較器IComparer。
class IntComparer extends IComparer[Int] {
override def compare(a: Int, b: Int): Boolean = {
return a < b
}
}
// 測試一下
def main(args: Array[String]): Unit = {
val list = List(9,1,6,3,5)
val sorter = new ListSorter[Int](list)
// 在調用sort方法時,傳入一個具體比較器對象。
println(sorter.sort(new IntComparer()))
}
}
在這個例子中,ListSorter需要外部提供一個比較方法。為了解決這個問題,面向對象的思路是:
- 對外部功能,定義了一個接口。並在接口中,聲明這個比較函數。
- ListSorter的sort函數,通過接口來使用外部的比較方法。
- 外部:定義了一個具體類,實現了這個接口。
- 調用者:在調用ListSorter的sort函數時,傳入一個具體類的對象。
“現在,我們的任務就是:把這個例子改成面向表達式的風格。”
“首先,把sort函數的輸入參數comparer變成一個函數類型。”
“這樣,我們就不需要IComparer,這個接口了。”
“IntComparer就可以從一個封裝類,變成一個帶比較函數的靜態類。”
函數式編程的第一個例子:
// 這個例子的主要功能是對一個List排序。
// 這是一個基於面向表達式的實現。
object Main {
// 一個支持排序的class。
// 這個class,需要外部提供一個比較函數。
class ListSorter[T](a: List[T]) {
def data: List[T] = a
def sort(f: (T, T) => Boolean): List[T] = {
return data.sortWith(f)
}
}
// 實現了一個比較函數。
object IntComparer {
def compare(a: Int, b: Int): Boolean = {
return a < b
}
}
def main(args: Array[String]): Unit = {
val list = List(9,1,6,3,5)
val sorter = new ListSorter[Int](list)
// use function rather than object
println(sorter.sort(IntComparer.compare))
// use function with lambda expression
println(sorter.sort( (a, b) => a < b ))
// use function with underscore
println(sorter.sort( _ < _ ))
// fluent infix style
println(sorter sort IntComparer.compare)
// fluent infix style with lambda expression
println(sorter sort {(a, b) => a < b})
// fluent infix style with underscore
println(sorter sort { _ < _ })
}
}
注:這里面實現了多種風格。
lambda expression,可以看成匿名函數的實現方法。
underscore: underscore在scala中有多種含義。這里是一種匿名函數的實現,scala會根據上下文推測"_"的含義。
infix style: 可以看出,不需要"."了。
“太好了,我們向函數式編程邁出了第一步!”
阿袁工作的第3天: 函數式編程:再純粹一些
“在昨天的例子中,我們還是實例化了ListSorter。”
“是啊,按照函數式編程的思想,我們需要把ListSorter的sort方法看成一個函數。”
“另外,我還學到了一點,在面向表達式風格中,不要寫return。最后一條expression的結果就應該是函數的返回值。”
“嗯,好的,我們繼續改改看。”
函數式編程的改進版:
// 這個例子的主要功能是對一個List排序。
// 這是一個基於面向表達式的實現。
// * Changed ListSorter as module
// * Do not use return
object Main {
object ListSorter {
def sort[T](a: List[T], f: (T, T) => Boolean): List[T] = {
// Do not use return
a.sortWith(f)
}
}
object IntComparer {
def compare(a: Int, b: Int): Boolean = {
// Do not use return
a < b
}
}
def main(args: Array[String]): Unit = {
val list = List(9,1,6,3,5)
// use function rather than object
println(ListSorter.sort(list, IntComparer.compare))
// use function with lambda
println(ListSorter.sort[Int](list, (a, b) => a < b))
// use function with underscore
println(ListSorter.sort[Int](list, _ < _))
}
}
發現了嗎? fluent infix style沒有了。這是因為,infix操作支持有一個參數的函數。
阿袁工作的第4天: 函數式編程:卷積(currying)
“fluent infix style有點接近人類的語言,使用好的話,可以增加可讀性。”
"但是,它也有個限制,只支持有一個參數的函數。"
“其實,卷積可以解決這個問題。"
"卷積?"
“給你舉個例子。一般的函數是這樣的。”
def normalFunc(a: Int, b: Int, c:Int): Int = {
a + b + c
}
"而卷積函數變成這樣,參數被分隔一個一個的。"
def curriedFunc(a: Int)(b: Int)(c:Int): Int = {
a + b + c
}
"卷積的思想是: 每次只給函數的一個參數賦值。這樣的一個主要用途是:局部函數(partial function application),
可以想象為把一個計算分成多個步驟計算(multiple stage computation)。這是調用的方法:"
// Usage: Currying in partial function application
val add2OneByOne = curriedFunc(1) _
// call a curried function variable with a normal arugment
println(add2OneByOne(2)(3))
// output: 6
“卷積帶來的一個附加益處,就是支持了多參數函數的infix操作。”
// Usage: call a curried function with an expression in fluent infix style
println(curriedFunc {1} {2} {3})
// output: 6
“scala真強大啊!我們繼續改改看。”
// 這個例子的主要功能是對一個List排序。
// 這是一個基於面向表達式的實現。
// * Using currying
object Main {
object ListSorter {
// curried function (a)(b)
def sort[T](a: List[T])(f: (T, T) => Boolean): List[T] = {
a.sortWith(f)
}
}
object IntComparer {
def compare(a: Int, b: Int): Boolean = {
a < b
}
}
def main(args: Array[String]): Unit = {
val list = List(9,1,6,3,5)
// use function rather than object
println(ListSorter.sort(list)(IntComparer.compare))
// use function with lambda expression
println(ListSorter.sort(list)((a, b) => a < b ))
// use function with underscore
println(ListSorter.sort(list)(_ < _ ))
// fluent infix style
println(ListSorter.sort {list} {IntComparer.compare})
// fluent infix style with lambda expression
println(ListSorter.sort {list} {(a, b) => a < b})
// fluent infix style with underscore
println(ListSorter.sort {list} { _ < _ })
// currying usage: partial function application
val sortWith = ListSorter.sort(list) _
// fluent infix style
println(sortWith(IntComparer.compare))
// fluent infix style with lambda expression
println(sortWith {(a, b) => a < b})
// fluent infix style with underscore
println(sortWith { _ < _ })
}
}
阿袁工作的第5天: 函數式編程:如何處理null
"今天有個新的認識。在面向對象語言中,我們經常使用null。但是在數學計算中,null是沒有意義的。"
“那么要使用什么呢?”
“如果返回值類型是一個集合,最好返回空集合。”
“如果返回值類型是一個值,scala提供了一個Option的泛型類,提供了一個None對象,表示返回的值是沒有值。”
“代碼示例如下。”
// 這個例子的主要功能是說明使用Nil和None、
object Main {
object NilNoneSample {
// 使用空集合。不要使用null。
def getEmptyList(): Seq[Int] = {
Nil
}
// 使用空集合。不要使用null。
def getEmptyVector(): Vector[Int] = {
Vector()
}
// 對於可能返回“沒有值”的結果,使用Option泛型類。
def getValueIfLargeThanZero(a: Int): Option[Int] = {
if (a > 0) Option(a) else None
}
}
def main(args: Array[String]): Unit = {
// use empty collection replace null
println(NilNoneSample.getEmptyList)
// output: List()
println(NilNoneSample.getEmptyVector)
// output: Vector()
// use None replace null
println(NilNoneSample.getValueIfLargeThanZero(1).get)
// output: 1
println(NilNoneSample.getValueIfLargeThanZero(-1).isEmpty)
// output: true
}
}
阿袁的日記
2016年9月X日 星期六
最近和阿靜接觸的機會多了很多。也學會了一些函數式編程的概念。
總結一下:
函數式編程的風格,即面向表達式編程風格,有如下要求:
- 把類看是算法的分類。
- 使用函數代替對象。
- 對於變量和參數,盡量使用:值(最好是不變的),Collection和函數等類型。
- 盡量使用不可變的數據類型。(重申一遍)
- 避免使用return語句。
- 對於集合類型,使用空集合來代替null。
- 對於其他數據類型,使用None代替null。
- 可以使用卷積來方便於多步驟計算的要求。
參照
- Functional programming
- 哥德爾不完備定理
- [英]安德魯・霍奇斯. 艾倫·圖靈傳 如謎的解謎者