go 學習筆記之學習函數語言程式設計前不要忘了函式基礎

snowdreams1006發表於2019-09-16

在程式設計世界中向來就沒有一家獨大的程式設計風格,至少目前還是百家爭鳴的春秋戰國,除了眾所周知的物件導向程式設計還有日漸流行的函數語言程式設計,當然這也是本系列文章的重點.

越來越多的主流語言在設計的時候幾乎無一例外都會參考函式式特性( lambda 表示式,原生支援 map,reduce...),就連面嚮物件語言的 Java8 也慢慢開始支援函數語言程式設計,所以再不學習函數語言程式設計可能就晚了!

go-functional-programming-about-function.jpg

但是在正式學習函數語言程式設計之前,不妨和早已熟悉的物件導向程式設計心底裡做下對比,通過對比學習的方式,相信你一定會收穫滿滿,因此特地整理出來關於 Go 語言的物件導向系列文章,邀君共賞.

上述系列文章講解了 Go 語言物件導向相關知識點,如果點選後沒有自動跳轉,可以關注微信公眾號「雪之夢技術驛站」檢視歷史文章,再次感謝你的閱讀與關注.

生物學家和數學家的立場不同

雖然是同一個世界,但是不同的人站在各自立場看問題,結果自然會千人千面,各有不同.

生物學家會下意識對動植物進行分類歸納,物件導向程式設計也是如此,用一系列的抽象模型去模擬現實世界的行為規律.

go-functional-programming-about-biology.jpg

數學家向來以嚴謹求學著稱,作為最重要的基礎科學,數學規律以及歸納演繹方法論對應的就是函數語言程式設計,不是模擬現實而是描述規律更有可能創造規律.

go-functional-programming-about-math.jpg

標準的函數語言程式設計具有濃厚的數學色彩,幸運的是,Go 並不是函式式語言,所以也不必受限於近乎苛責般的條條框框.

簡單來說,函數語言程式設計具有以下特點:

  • 不可變性: 不用狀態變數和可變物件
  • 函式只能有一個引數
  • 純函式沒有副作用

go-functional-programming-about-feature.jpg

摘自維基百科中關於函數語言程式設計中有這麼一段話:

In computer science, functional programming is a programming paradigm—a style of building the structure and elements of computer programs—that treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data.

上述的英文的大致意思是說:函數語言程式設計將計算機程式看成是數學函式的推演,不用狀態變數也不用可變物件來表達數與數之間的關係.

如需瞭解詳情,可點選訪問維基百科關於函數語言程式設計 Functional programming 的相關介紹.

函數語言程式設計的立足點和出發點是函式,複雜函式是基本函式經過一定組合規律形成的,所以描述複雜函式的過程就是如何拆解重組的過程.

所以接下來我們一邊複習一邊學習函式的基本特點,為接下來理解函數語言程式設計打下基礎,關於函式的基礎語言可參考 go 學習筆記之值得特別關注的基礎語法有哪些

函式的基礎語法和高階特性

下面以最基本四則運算為例,貫穿全文講解函式的基本語法和高階特性,力求做到知其然知其所以然.

  • func 定義普通函式

eval 函式定義了加減乘除基本運算規則,若不支援操作型別則丟擲異常,終止程式.

func eval(a, b int, op string) int {
    var result int
    switch op {
    case "+":
        result = a + b
    case "-":
        result = a - b
    case "*":
        result = a * b
    case "/":
        result = a / b
    default:
        panic("unsupported operator: " + op)
    }
    return result
}

測試未定義操作取餘 % 運算時,則丟擲異常,unsupported operator: % ,說明僅僅支援加減乘除基本運算.

func TestEval(t *testing.T) {
    // 3 -1 2 0 unsupported operator: %
    t.Log(
        eval(1, 2, "+"),
        eval(1, 2, "-"),
        eval(1, 2, "*"),
        eval(1, 2, "/"),
        eval(1, 2, "%"),
    )
}
  • 多返回值定義標準函式

Go 語言和其他主流的程式語言明顯不同的是,函式支援多返回值,通常第一個返回值表示真正結果,第二個返回值表示是否錯誤,這也是 Go 關於異常錯誤設計的獨特之處.

如果正常返回,則表示沒有錯誤,那麼第一個返回值是正常結果而第二個返回值則是空 nil;如果異常返回,第一個返回值設計無意義的特殊值,第二個返回值是具體的錯誤資訊,一般非 nil.

func evalWithStandardStyle(a, b int, op string) (int, error) {
    switch op {
    case "+":
        return a + b, nil
    case "-":
        return a - b, nil
    case "*":
        return a * b, nil
    case "/":
        return a / b, nil
    default:
        return 0, fmt.Errorf("unsupported operator: %s", op)
    }
}

改造 eval 函式以編寫真正 Go 程式,此時再次測試,結果顯示遇到沒有定義的操作符時不再丟擲異常而是返回預設零值以及給出簡短的錯誤描述資訊.

func TestEvalWithStandardStyle(t *testing.T) {
    // Success: 2
    if result, err := evalWithStandardStyle(5, 2, "/"); err != nil {
        t.Log("Error:", err)
    } else {
        t.Log("Success:", result)
    }

    // Error: unsupported operator: %
    if result, err := evalWithStandardStyle(5, 2, "%"); err != nil {
        t.Log("Error:", err)
    } else {
        t.Log("Success:", result)
    }
}
  • 其他函式作為引數傳入

上例通過多返回值解決了遇到不支援的運算子會報錯終止程式的問題,但是並沒有真正解決問題,假如真的想要進行非預定義的運算時,同樣是無能為力!

誰讓你只是使用者而不是設計者呢!

那麼舞臺交給你,你就是主角,你想要怎麼處理輸入怎麼輸出就怎麼處理,全部邏輯轉移給使用者,這樣就不存在無法滿足需求的情況了.

func evalWithApplyStyle(a, b int, op func(int, int) (int, error)) (int, error) {
    return op(a, b)
}

操作符由原來的字串 string 更改成函式 func(int, int) (int, error),舞臺交給你,全靠自由發揮!

evalWithApplyStyle 函式內部直接呼叫函式引數 op 並返回該函式的處理結果,當前演示示例中函式的控制權完全轉移給函式入參 op 函式,實際情況可按照實際需求決定如何處理 evalWithApplyStyle 邏輯.

func divide(a, b int) (int, error) {
    return a / b, nil
}

func mod(a, b int) (int, error) {
    return a % b, nil
}

自己動手,豐衣足食,順手定義除法 divide 和取餘 mod 運算,接下來測試下實現效果.

func TestEvalWithApplyStyle(t *testing.T) {
    // Success: 2
    if result, err := evalWithApplyStyle(5, 2, divide); err != nil {
        t.Log("Error:", err)
    } else {
        t.Log("Success:", result)
    }

    // Success: 1
    if result, err := evalWithApplyStyle(5, 2, mod); err != nil {
        t.Log("Error:", err)
    } else {
        t.Log("Success:", result)
    }
}

測試結果很理想,不僅實現了減加乘除等基本運算,還可以實現之前一直沒法實現的取餘運算!

這說明了這種函式作為引數的做法充分調動勞動人民積極性,媽媽再也不用擔心我無法實現複雜功能了呢!

  • 匿名函式也可以作為引數

一般而言,呼叫函式時都是直接用函式名進行呼叫,單獨的函式具有可複用性,但如果本就是一次性函式的話,其實是沒必要定義帶函式名形式的函式.

依然是上述例子,這一次對兩個數的運算規則不再是數學運算了,這一次我們來比較兩個數的最大值,使用匿名函式的形式進行實現.

func TestEvalWithApplyStyle(t *testing.T) {
    // Success: 5
    if result, err := evalWithApplyStyle(5, 2, func(a int, b int) (result int, e error) {
        if a > b {
            return a, nil
        }
        return b, nil
    }); err != nil {
        t.Log("Error:", err)
    } else {
        t.Log("Success:", result)
    }
}
  • 函式的返回值可以是函式

依然是上述示例,如果由於原因不需要立即返回函式的計算結果而是等待使用者自己覺得時機合適的時候再計算返回值,這時候函式返回值依然是函式就很有作用了,也就是所謂的惰性求值.

func evalWithFunctionalStyle(a, b int, op func(int, int) (int, error)) func() (int, error) {
    return func() (int, error) {
        return op(a, b)
    }
}

上述函式看起來可能有點難以理解,實際上相對於上例僅僅更改了返回值,由原來的 (int, error) 更改成 func() (int, error) ,其餘均保持不變喲!

evalWithFunctionalStyle 函式依然是使用者的主場,和上例相比的唯一不同之處在於,你的主場你做主,什麼時候裁判完全自己說了算,並不是執行後就立馬宣佈結果.

func pow(a, b int) (int, error) {
    return int(math.Pow(float64(a), float64(b))),nil
}

func TestEvalWithFunctionalStyle(t *testing.T) {
    ef := evalWithFunctionalStyle(5, 2, pow)

    time.Sleep(time.Second * 1)

    // Success: 25
    if result, err := ef(); err != nil {
        t.Log("Error:", err)
    } else {
        t.Log("Success:", result)
    }
}

time.Sleep(time.Second * 1) 演示程式碼代表執行 evalWithFunctionalStyle 函式後可以不立即計算最終結果,等待時機合適後由使用者再次呼叫 ef() 函式進行惰性求值.

// 1 1 2 3 5 8 13 21 34 55
//     a b
//       a b
func fibonacci() func() int {
    a, b := 0, 1
    return func() int {
        a, b = b, a+b
        return a
    }
}
  • 函式可以充當型別

上述示例中講解了函式可以作為返回值,引數有函式,返回值也有引數,所以 evalWithFunctionalStyle 函式看起來比較費勁,而 Go 語言的型別別名就是為了簡化而生的,更何況函式是 Go 中的一等公民,當然也適合了.

func evalWithFunctionalStyle(a, b int, op func(int, int) (int, error)) func() (int, error) {
    return func() (int, error) {
        return op(a, b)
    }
}

於是打算把入參函式 func(int, int) (int, error) 和返回值函式 func() (int, error) 進行統一,而入參函式和返回值函式唯一不同之處就是入參個數不同,所以順理成章想到了 Go 函式中的不定長引數相關語法.

type generateIntFunc func(base ...int) (int, error)

這樣入參函式和出參函式都可以用 generateIntFunc 型別函式進行替代,接著改造 evalWithFunctionalStyle 函式.

func evalWithObjectiveStyle(a, b int, op generateIntFunc) generateIntFunc {
    return func(base ...int) (i int, e error) {
        return op(a, b)
    }
}

改造後的 evalWithObjectiveStyle 函式看起來比較簡潔,花花架子中看是否中用還不好說,還是用測試用例說話吧!

func TestEvalWithObjectiveStyle(t *testing.T) {
    ef := evalWithObjectiveStyle(5, 2, func(base ...int) (int,error) {
        result := 0
        for i := range base {
            result += base[i]
        }
        return result,nil
    })

    time.Sleep(time.Second * 1)

    // Success: 7
    if result, err := ef(); err != nil {
        t.Log("Error:", err)
    } else {
        t.Log("Success:", result)
    }
}

函式別名進行型別化後並不影響功能,依然是函數語言程式設計,不過夾雜了些物件導向的味道.

  • 型別化函式可以實現介面

函式通過別名形式進行型別化後可以實現介面,某些程度上可以視為一種型別,因此實現介面也是順理成章的事情.

func (g generateIntFunc) String() string {
    r,_ := g()
    return fmt.Sprint(r)
}

此處示例程式碼中為型別化函式 generateIntFunc 實現 String 介面方法,可能並沒有太大實際意義,僅僅是為了講解這個知識點而硬湊上去的,實際情況肯定會有所不同.

func TestEvalWithInterfaceStyle(t *testing.T) {
    ef := evalWithObjectiveStyle(5, 2, func(base ...int) (int,error) {
        result := 0
        for i := range base {
            result += base[i]
        }
        return result,nil
    })

    time.Sleep(time.Second * 1)

    // String: 7
    t.Log("String:", ef.String())

    // Success: 7
    if result, err := ef(); err != nil {
        t.Log("Error:", err)
    } else {
        t.Log("Success:", result)
    }
}

惰性求值獲取的函式變數 ef 此時可以呼叫 String 方法,也就是具備物件化能力,得到的最終結果竟然和直接執行該函式的值一樣?

有點神奇,目前還不理解這是什麼操作,如果有 Go 語言的大佬們不吝賜教的話,小弟感激不盡!

  • 水到渠成的閉包

函式的引數,返回值都可以是另外的函式,函式也可以作為引用那樣傳遞給變數,也存在匿名函式等簡化形式,除此之外,型別化後的函式還可以用來實現介面等等特性應該足以闡釋一等公民的高貴身份地位了吧?

如此強大的函式特性,只要稍加組合使用就會擁有強大的能力,並且 Go 語言並不是嚴格的函式式語言,沒有太多語法層面的限制.

// 1 1 2 3 5 8 13 21 34 55
//     a b
//       a b
func fibonacci() func() int {
    a, b := 0, 1
    return func() int {
        a, b = b, a+b
        return a
    }
}

斐波那契數列函式 fibonacci 的返回值是真正的生成器函式,每次呼叫都會生成新的斐波那契數字.

這就是 Go 語言實現閉包的一種簡單示例,fibonacci 函式本身的變數 a,b 被內部匿名函式 func() int 所引用,而這種引用最終被使用者不斷呼叫就會導致最初的 a,b 變數一直被佔用著,只要繼續呼叫這種生成器,裴波那契數列的數字就會一直遞增.

// 1 1 2 3 5 8 13 21 34 55
func TestFibonacci(t *testing.T) {
    f := fibonacci()
    for i := 0; i < 10; i++ {
        fmt.Print(f(), " ")
    }
    fmt.Println()
}
func TestFibonacci(t *testing.T) {
    f := fibonacci()
    for i := 0; i < 10; i++ {
        fmt.Print(f(), " ")
    }
    fmt.Println()
}

go-functional-programming-about-fib.png

函數語言程式設計入門函式總結

  • 函式是一等公民,其中函式引數,變數,函式返回值都可以是函式.
  • 高階函式是普通函式組合而成,引數和返回值可以是另外的函式.
  • 函式是函數語言程式設計的基礎,支援函數語言程式設計但並不是函式式語言.
  • 沒有純粹函數語言程式設計的條條框框,更加靈活自由,良好的可讀性.

雪之夢技術驛站.png

相關文章