再談函數式編程:釋放編程創造力

當抽象程度足夠高,編程就能接近數學的優雅。

在“Go 模板:用代碼生成代碼”一文中,談到了生成器模式的實現。 先 Copy 如下:

生成器模式(Builder)

假設我們要造一輛車,車有車身、引擎、座位、輪子。Go 的生成器模式的代碼是這樣子的:

package model

import "fmt"

type ChinaCar struct {
    Body   string
    Engine string
    Seats  []string
    Wheels []string
}

func newChinaCar(body string, engine string, seats []string, wheels []string) *ChinaCar {
    return &ChinaCar{
        Body:   body,
        Engine: engine,
        Seats:  seats,
        Wheels: wheels,
    }
}

type CarBuilder struct {
    body   string
    engine string
    seats  []string
    wheels []string
}

func ChinaCharBuilder() *CarBuilder {
    return &CarBuilder{}
}

func (b *CarBuilder) Build() *ChinaCar {
    return newChinaCar(b.body, b.engine, b.seats, b.wheels)
}

func (b *CarBuilder) Body(body string) *CarBuilder {
    b.body = body
    return b
}

func (b *CarBuilder) Engine(engine string) *CarBuilder {
    b.engine = engine
    return b
}

func (b *CarBuilder) Seats(seats []string) *CarBuilder {
    b.seats = seats
    return b
}

func (b *CarBuilder) Wheels(wheels []string) *CarBuilder {
    b.wheels = wheels
    return b
}

func main() {
    car := ChinaCharBuilder().
        Body("More advanced").
        Engine("Progressed").
        Seats([]string{"good", "nice"}).
        Wheels([]string{"solid", "smooth"}).
        Build()
    fmt.Printf("%+v", car)
}

生成器模式怎麼寫?遵循三步即可:

(1) 先構造一個對應的生成器,這個生成器與目標對象有一樣的屬性;
(2) 對於每一個屬性,有一個方法設置屬性,然後返回生成器的引用本身;
(3) 最後調用生成器的 Build 方法,這個方法會調用目標對象的構造器來生成目標對象。

選項器模式(Optional)

與生成器模式類似,還有一種,稱之爲“選項器模式”。代碼如下:

看上去是不是結構和生成器模式很像?但兩者的用途完全不同:

  • 生成器模式用於構建由多個子部件共同組成的複雜整體;子部件可能有緊密的交互關係。
  • 選項器模式靈活組合多個選項。選項之間沒有交互關係。
type ElementOperationResultQuery struct {
	AgentId    string
	ElementId  string
	ElementIds []string
}

type Option func(c *ElementOperationResultQuery)

type ElementOperationResultQueryOption struct {
	Opts ElementOperationResultQuery
}

func AgentId(agentId string) Option {
	return func(opts *ElementOperationResultQuery) {
		opts.AgentId = agentId
	}
}

func ElementId(elementId string) Option {
	return func(opts *ElementOperationResultQuery) {
		opts.ElementId = elementId
	}
}

func ElementIds(elementIds []string) Option {
	return func(opts *ElementOperationResultQuery) {
		opts.ElementIds = elementIds
	}
}

func NewElementOperationResultQuery(opts ...Option) *ElementOperationResultQuery {
	elementOperationResultQuery := ElementOperationResultQuery{}
	for _, opt := range opts {
		// 函數指針的賦值調用
		opt(&elementOperationResultQuery)
	}
	return &elementOperationResultQuery
}

func main() {
	query := NewElementOperationResultQuery(AgentId("abc"), ElementId("bcd"))
	fmt.Println(query)
}

流水線模式(Pipeline)

對選項器模式稍加改造,就可以發現其中蘊藏的 Pipeline 模式。

將上面的 ElementOperationResultQuery 裏的數據換成列表或 Context 對象,將函數換成數據處理函數,就變成了如下模式:

package main

import (
	"fmt"
	"sort"
)

type Data struct {
	List []int
}

type SubRoutine func(c *Data) *Data

func Add(i int) SubRoutine {
	return func(opts *Data) *Data {
		for k, _ := range opts.List {
			opts.List[k] = opts.List[k] + i
		}
		return opts
	}
}

func Multi(j int) SubRoutine {
	return func(opts *Data) *Data {
		for k, _ := range opts.List {
			opts.List[k] = opts.List[k] * j
		}
		return opts
	}
}

func Sort() SubRoutine {
	return func(opts *Data) *Data {
		sort.Ints(opts.List)
		return opts
	}
}

func Pipeline(data Data, opts ...SubRoutine) *Data {
	var result = &data
	for _, opt := range opts {
		// 函數指針的賦值調用
		result = opt(result)
	}
	return result
}

func main() {
	data := Data{[]int{2, 5, 7, 8, 6}}
	changed := Pipeline(data, Add(1), Multi(2))
	fmt.Println(*changed)

	changed2 := Pipeline(data, Multi(3), Sort(), Add(2))
	fmt.Println(*changed2)
}

隱隱感到:“閉包函數 + 函數式編程 + 指針”的組合,蘊藏着強大的編程表達能力。

閉包

這裏講講閉包的神奇力量。我們知道,函數裏的局部變量,在函數調用返回之後,就會銷燬。但是如果函數裏有一個閉包函數,這個閉包函數引用了函數裏的局部變量,在外層函數返回之後,這個局部變量卻不會銷燬。一個利用閉包實現的簡單計數器如下:

package main

import (
	"fmt"
	"os"
)

func count_down() func() {
	i := 10
	return func() {
		i--
		fmt.Println(i)
		if i == 0 {
			fmt.Println("count down to zero")
			os.Exit(0)
		}
	}
}

func main() {
	cd := count_down()
	for {
		cd()
	}
}

函數式編程的強大威力

先溫習下“函數式+泛型編程:編寫簡潔可複用的代碼”,咱們來看看函數式編程 + 泛型能夠產生怎樣的表達能力。

利用閉包,很容易實現多元函數(柯里化):

package main

import (
	"fmt"
	"sort"
	"strings"
)

func Curry[T any, S any, R any](list []T, f func(T) S) func(func([]S) R) R {
	return func(ff func([]S) R) R {
		ss := make([]S, 0)
		for _, e := range list {
			ss = append(ss, f(e))
		}
		return ff(ss)
	}
}

type Teacher struct {
	Id   string
	Name string
}

func main() {
	teachers := []Teacher{{Id: "2003111220", Name: "fangqing"}, {Id: "2003111229", Name: "xiaoni"}}
	namef := Curry[Teacher, string, string](teachers, func(t Teacher) string { return t.Name })
	joinf := func(list []string) string { return strings.Join(list, ",") }
	result := namef(joinf)
	fmt.Println(result)

	idf := Curry[Teacher, string, []string](teachers, func(t Teacher) string { return t.Id })
	sortf := func(list []string) []string { sort.Strings(list); return list }
	result2 := idf(sortf)
	fmt.Println(result2)

}

這個 Curry 可能不太好理解。它先使用映射函數 f func(T) S 將一個 []T 轉成 []S,得到一個函數。這個函數再接收另一個函數 func([]S) R,最終得到 R。

如果拆解成這兩個函數的組合,可能就容易理解了:

func Convert[T any, S any](list []T, f func(T) S) []S {
	ss := make([]S, 0)
	for _, e := range list {
		ss = append(ss, f(e))
	}
	return ss
}

func Collect[S any, R any](ss []S, c func([]S) R) R {
	return c(ss)
}

如果這樣還不太明顯的話,可以將函數定義爲自定義類型:

type MapFunc[T any, S any] func(t T) S
type CollectFunc[S any, R any] func([]S) R

func Curry2[T any, S any, R any](list []T, f MapFunc[T, S]) func(CollectFunc[S, R]) R {
	return func(collectFunc CollectFunc[S, R]) R {
		ss := make([]S, 0)
		for _, e := range list {
			ss = append(ss, f(e))
		}
		return collectFunc(ss)
	}
}

idf3 := Curry2[Teacher, string, []string](teachers, func(t Teacher) string { return t.Id })
sortf3 := func(list []string) []string { sort.Strings(list); return list }
result3 := idf3(sortf3)
fmt.Println(result3)

裏面那個遍歷也可以進一步抽象。這個函數可以進一步抽象:

type ListMapFunc[T any, S any] func(list []T, mapFunc MapFunc[T, S]) []S

func Curry3[T any, S any, R any](list []T, listMapFunc ListMapFunc[T, S], mapFunc MapFunc[T, S]) func(CollectFunc[S, R]) R {
	return func(collectFunc CollectFunc[S, R]) R {
		return collectFunc(listMapFunc(list, mapFunc))
	}
}

func MapList[T any, S any](list []T, f MapFunc[T, S]) []S {
	ss := make([]S, 0)
	for _, e := range list {
		ss = append(ss, f(e))
	}
	return ss
}

id4 := func(t Teacher) string { return t.Id }
idf4 := Curry3[Teacher, string, []string](teachers, func(teachers []Teacher, mapFunc MapFunc[Teacher, string]) []string { return MapList(teachers, mapFunc) }, id4)
sortf4 := func(list []string) []string { sort.Strings(list); return list }
result4 := idf4(sortf4)
fmt.Println(result4)

可以看到,藉助 “閉包 + 函數式編程 + 泛型+ 柯里化 + 自定義函數類型”, 可以獲得了很強大的表達能力。多練習,對編程思維的提升大有裨益。

附記

用AI 能寫出來麼?【使用通義千問】

程序員啊,準備擇日退休吧!想一想,一個普通的AI 能夠在短短十秒內寫出一個一流程序員才能寫出的一流程序,你還掙扎什麼呢?

​軟件開發領域就那幾件事,一旦每一件事都找到 Al 的方法,再串聯起來,需求自動化完成就爲期不遠了。

​當然,程序員不會自甘退出舞臺的,畢竟,他們纔是最有可能掌握 AI 力量的種族。奪走你工作的不是 AI ,而是那些富有經驗和直覺,思維高度活躍敏銳的善於利用 AI 力量的人。

小結

本文從生成器模式開始談起,講到與之相似的選項器模式,擴展成 Pipeline 模式,最後給出閉包及閉包加函數式編程組合的編程表達能力。

當你能夠玩轉函數式編程時,就獲得了非常強大的編程表達能力。編程將再一次展示其魅力和樂趣。

函數式編程的關鍵在於抽象。當抽象程度足夠高,編程就能接近數學的優雅。

參考資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章