函数式+泛型编程:编写简洁可复用的代码

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)

是不是很嗨!

可以看到,仅仅只是通过函数组合,可以构建出非常强大的功能,而且可以在这个基础上不断叠加组合。

当你熟谙函数式编程时,只要几个函数,就可以构建出一个简洁易用的处理框架。真是一项迷人的技艺啊!

小结

函数式 + 泛型编程,是一对强大的编程组合,威力极强,可谓重剑钝锋。使用函数式+泛型编程,编写简洁可复用的代码,也是编程乐趣之一。

posted @ 2023-11-19 10:41  琴水玉  阅读(128)  评论(0编辑  收藏  举报