函數式編程 : 一個程序猿進化的故事


[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。
  • 可以使用卷積來方便於多步驟計算的要求。

參照


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM