再谈函数式编程:释放编程创造力

当抽象程度足够高,编程就能接近数学的优雅。

在“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 模式,最后给出闭包及闭包加函数式编程组合的编程表达能力。

当你能够玩转函数式编程时,就获得了非常强大的编程表达能力。编程将再一次展示其魅力和乐趣。

函数式编程的关键在于抽象。当抽象程度足够高,编程就能接近数学的优雅。

参考资料

posted @ 2024-01-28 10:47  琴水玉  阅读(53)  评论(0编辑  收藏  举报