函数式+泛型编程:编写简洁可复用的代码
Write Less Do More.
引子
我个人比较信奉的一句编程箴言: Write Less and Do More。无论是出于懒,还是出于酷炫的编程技艺,或者是一种编程乐趣。
函数式和泛型编程是编写简洁可复用代码的两大编程技艺,组合起来威力更加强大。另一项技艺是元编程。本文主要来讲讲函数式和泛型编程。
泛型编程
所谓泛型函数,就是一个函数适用于多种类型。有很多流程算法,都是可以适配多种类型的,比如加减之于整数/实数/复数、排序之于不同类型的数组。这些很适合用泛型来表达。
泛型和普通函数和很相似,只有一点不同。
一个简单的例子
比如 如下代码:
func add(a int, b int) int {
return a + b
}
func addInt8(a int8, b int8) int8 {
return a + b
}
func main() {
fmt.Println(add(1, 2))
fmt.Println(addInt8(1, 2))
}
add 和 addInt8 只是类型不同,实际上算法一模一样。这时候就适合用泛型改造一下:
func addGeneric[T int | int8 | int32 | int64](a T, b T) T {
return a + b
}
func main() {
fmt.Println(addGeneric(1, 2))
fmt.Println(addGeneric(int8(1), int8(2)))
fmt.Println(addGeneric(int32(1), int32(2)))
fmt.Println(addGeneric(int64(1), int64(2)))
}
注意到普通函数和泛型函数只有一点点差别。就是方法名和参数列表之间多了个类型形参 [T int|int8|int32|int64]。 这个类型形参可以替代实参里的参数类型,返回值如有必要也替换 T。
如果你不太适应写泛型函数,可以先写个普通函数,然后再加上类型形参,再把实参里的类型替换成类型形参即可。就这么简单!多写几次就会了。
封装库函数
泛型函数很适合封装库函数。比如如下代码,在 byte[] 和对象之间转换。
package util
import (
"bytes"
"encoding/gob"
)
/*
* 使用 gob 进行 struct{} 与 byte[] 之间转换
* 只适用于 Go, 不支持函数和通道
*/
func ConvertToBytes[T any](t *T) []byte {
var buf bytes.Buffer
e := gob.NewEncoder(&buf)
err := e.Encode(*t)
if err != nil {
panic(err)
}
return buf.Bytes()
}
func ConvertFromBytes[T any](buf []byte) *T {
var obj T
d := gob.NewDecoder(bytes.NewReader(buf))
err := d.Decode(&obj)
if err != nil {
panic(err)
}
return &obj
}
测试用例如下:
type HostCacheInfo2 struct {
TenantId string
AgentId string
OsType string
DisplayIp string
GroupId string
Hostname string
PlatformType string
}
func TestConversion(t *testing.T) {
hc := HostCacheInfo2{AgentId: "agentId", TenantId: "tenantId", PlatformType: "SERVER", Hostname: "qin"}
bytes := util.ConvertToBytes(&hc)
fmt.Println(bytes)
hc2 := util.ConvertFromBytes[HostCacheInfo2](bytes)
fmt.Println(hc2)
str := "abcde"
bytes2 := util.ConvertToBytes(&str)
str2 := util.ConvertFromBytes[string](bytes2)
fmt.Println(str2)
}
如下代码所示,基于 golang BigCache 库实现一个易用的对象本地缓存。BigCache 底层是用字节数组来存储的,对于存储对象不太友好。
package util
import (
"github.com/allegro/bigcache"
)
type LocalCache[T any] struct {
LocalCache *bigcache.BigCache
}
func NewLocalCache[T any](config bigcache.Config) *LocalCache[T] {
bigCache, _ := bigcache.NewBigCache(config)
return &LocalCache[T]{LocalCache: bigCache}
}
func (c *LocalCache[T]) Set(key string, value T) {
c.LocalCache.Set(key, ConvertToBytes(&value))
}
func (c *LocalCache[T]) Get(key string) *T {
bytes, _ := c.LocalCache.Get(key)
return ConvertFromBytes[T](bytes)
}
测试用例如下:
package test
import (
"fmt"
"testing"
"time"
"util"
"github.com/allegro/bigcache"
)
func TestLocalCache(t *testing.T) {
lc := util.NewLocalCache[HostCacheInfo2](bigcache.DefaultConfig(10 * time.Minute))
lc.Set("abc", HostCacheInfo2{AgentId: "agentId", TenantId: "tenantId"})
host := lc.Get("abc")
fmt.Println(*host)
}
func TestLocalCache2(t *testing.T) {
lc := util.NewLocalCache[string](bigcache.DefaultConfig(10 * time.Minute))
lc.Set("abc", "a dream")
astring := lc.Get("abc")
fmt.Println(*astring)
}
掌握泛型编程,你的编程技艺会更上一层楼。
函数式编程
函数式编程看上去很神秘,但其实很简单。有一定编程经验的人都知道,函数可以接受参数进行计算。大多数时候,参数可能就是普通的具体的值,按照一定的规则进行计算。函数式编程,不过就是把函数地址传给函数,然后可以调用传入的函数而已。
小试牛刀
我比较喜欢用的例子是,取出某个对象列表里的某个元素。你不知道这个对象的类型是什么,只知道如何从对象里取这个元素。
如下代码所示,只需要短短 6 行代码,你可以从任意对象列表中获取对象的某个属性的列表。酷不酷?
func GetElements[E any, R any](objlist []E, convertFunc func(e E) R) []R {
result := make([]R, len(objlist))
for _, e := range objlist {
result = append(result, convertFunc(e))
}
return result
}
测试用例
type Person struct {
Name string
Age int
}
func NewPerson(name string, age int) Person {
return Person{Name: name, Age: age}
}
type Student struct {
No string
}
func NewStudent(no string) Student {
return Student{No: no}
}
func main() {
persons := []Person{NewPerson("qin", 35), NewPerson("ni", 27)}
fmt.Println(GetElements(persons, func(p Person) string {
return p.Name
}))
students := []Student{NewStudent("S001"), NewStudent("S003")}
fmt.Println(GetElements(students, func(s Student) string {
return s.No
}))
}
如果支持元编程(用反射也可以做到)的话,还可以把属性名传入函数,就能实现在任意列表取出对象的任意属性来生成一个新的列表。
你还可以给这个函数添加更多的功能。比如过滤:
func GetElementsWithFilter[E any, R any](objlist []E,
convertFunc func(e E) R,
filterFunc func(r R) bool) []R {
result := make([]R, 0)
for _, e := range objlist {
r := convertFunc(e)
if (filterFunc(r)) {
result = append(result, r)
}
}
return result
}
测试用例
oldPersonAges := GetElementsWithFilter(persons, func(p Person) int { return p.Age }, func(age int) bool { return age > 35 })
fmt.Println(oldPersonAges)
文件处理
我们来实现一个通用文件读写工具。读取文本文件的每一行,使用一个外部函数来处理。
func ScanFile(filename string) *bufio.Scanner {
readFile, err := os.Open(filename)
if err != nil {
fmt.Println(err)
}
fileScanner := bufio.NewScanner(readFile)
fileScanner.Split(bufio.ScanLines)
return fileScanner
}
func ReadAndHandle(filename string, handle func(line string)) {
fileScanner := ScanFile(filename)
for fileScanner.Scan() {
handle(fileScanner.Text())
}
}
func main() {
ReadAndHandle("/Users/qinshu/Development/ids_method_costs_20230729182500015.txt", func(line string) {
fmt.Println(line + " chars: " + strconv.Itoa(len(line)))
})
}
假设要读取一批文件呢?这一批文件是通过不同方式来获取的。只消这样:
func BatchReadAndHandle(filenamesGenerator func() []string, handle func(line string)) {
for _, filename := range filenamesGenerator() {
ReadAndHandle(filename, handle)
}
}
用法:
batchGetFilenameFunc := func() []string {
cmd := exec.Command("/bin/bash", "-c", "ls -1 /Users/qinshu/Development/ids_method_costs_*.txt")
out, err := cmd.CombinedOutput()
if err != nil {
fmt.Printf("combined out:\n%s\n", string(out))
log.Fatalf("cmd.Run() failed with %s\n", err)
}
fmt.Printf("combined out:\n%s\n", string(out))
return strings.Split(string(out), "\n")
}
BatchReadAndHandle(batchGetFilenameFunc, func(line string) {
fmt.Println(line + " chars: " + strconv.Itoa(len(line)))
})
现在,我要写一个文件处理的简易框架,它的过程如下:
- 第一步:拿到一个文件名列表;
- 第二步:过滤文件名,拿到所需的文件名;
- 第三步:对文件的每一行进行处理,输出一个值;
- 第四步:对第三步输出的值,进行聚合,输出一个最终聚合的值。
func ReadAndHandleWithReturn[T any](filename string, handle func(line string) T) []T {
fileScanner := ScanFile(filename)
result := make([]T, 0)
for fileScanner.Scan() {
result = append(result, handle(fileScanner.Text()))
}
return result
}
func handleFiles[T any, R any](
filenamesGenerator func() []string,
filenameFilter func(filename string) bool,
handle func(line string) T,
aggregate func(t []T) R) R {
var r R
for _, filename := range filenamesGenerator() {
if filenameFilter(filename) {
subresults := ReadAndHandleWithReturn[T](filename, handle)
r = aggregate(subresults)
}
}
return r
}
使用:
charsCount := func(line string) int {
return len(line)
}
aggregate := func(numbers []int) int {
total := 0
for _, number := range numbers {
total += number
}
return total
}
totalChars := handleFiles[int, int](batchGetFilenameFunc, filenameFilter, charsCount, aggregate)
fmt.Println("totalChars: " + strconv.Itoa(totalChars))
lineConcat := func(line string) string { return line }
lineAggregate := func(lines []string) string {
return strings.Join(lines, "\n")
}
totalLines := handleFiles[string, string](batchGetFilenameFunc, filenameFilter, lineConcat, lineAggregate)
fmt.Println("totalLines: " + totalLines)
是不是很嗨!
可以看到,仅仅只是通过函数组合,可以构建出非常强大的功能,而且可以在这个基础上不断叠加组合。
当你熟谙函数式编程时,只要几个函数,就可以构建出一个简洁易用的处理框架。真是一项迷人的技艺啊!
小结
函数式 + 泛型编程,是一对强大的编程组合,威力极强,可谓重剑钝锋。使用函数式+泛型编程,编写简洁可复用的代码,也是编程乐趣之一。