當抽象程度足夠高,編程就能接近數學的優雅。
在“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 模式,最後給出閉包及閉包加函數式編程組合的編程表達能力。
當你能夠玩轉函數式編程時,就獲得了非常強大的編程表達能力。編程將再一次展示其魅力和樂趣。
函數式編程的關鍵在於抽象。當抽象程度足夠高,編程就能接近數學的優雅。