首页 > 编程语言 >函数式+泛型编程:编写简洁可复用的代码

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

时间:2023-11-19 10:58:04浏览次数:40  
标签:return string fmt 编程 复用 Println func 泛型

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)

是不是很嗨!

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

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

小结

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

标签:return,string,fmt,编程,复用,Println,func,泛型
From: https://www.cnblogs.com/lovesqcc/p/17841709.html

相关文章

  • 使用 ChatGPT 帮助小学生编程入门系列之一:Python 编程读取和解析天气预报网页上的数据
    现在国内小学生也开设了信息技术课,课程内容也涉及到了一些简单的编程实践,比如Scratch和Python.当初这个公众号申请时专门用了我儿子的名字,算是抢注吧,毕竟微信公众号和其他社交媒体平台不一样,不允许重名。我也曾经和我儿子聊过,我今年都40多岁了,这个公众号将来迟早有一天会正......
  • 小学四则运算编程实践(选做)
    从《构建之法》第一章的“程序”例子出发,像阿超那样,花二十分钟写一个能自动生成小学四则运算题目的命令行“软件”,满足以下需求:(以下参考博客链接:http://www.cnblogs.com/jiel/p/4810756.html)include<stdio.h>include<stdlib.h>include<time.h>voidgenerate_arithmetic......
  • 【教3妹学编程-java基础6】详解父子类变量、代码块、构造函数执行顺序
    -----------------第二天------------------------本文先论述父子类变量、代码块、构造函数执行顺序的结论,然后通过举例论证,接着再扩展,彻底搞懂静态代码块、动态代码块、构造函数、父子类、类加载机制等知识体系。温故而知新,建议点赞收藏~ 1先说结论 面试官:好的,你说一下java中父......
  • 【教3妹学编程-算法题】三个无重叠子数组的最大和
    2哥 :3妹,咋啦?一副苦大仇深的样子?3妹:不开心呀不开心,羽生结弦宣布离婚。2哥 :羽生什么?3妹:羽生结弦!2哥 :什么结弦?3妹:羽生结弦!!!2哥:羽生结弦是谁?他离婚关你啥事啊?3妹:你不知道,他是日本著名花滑运动员,前几个月刚宣布结婚,没想到这么快就离了。真是短时间内震惊我两次!2哥 :哎,人家怎......
  • 熟悉编程语言
    现在最受欢迎的编程语言top50这50种编程语言的编程泛型命令式:Swift,Ada,C++面向过程:Fortran,Pascal,Lua,C面向对象:Python,C++,Java,E,Agora,Ruby,F#,COBOL,PHP,go,Objective-C声明式:SQL,CSS函数式:Lisp,Scala,logo,R,ML,Haskell,Scheme逻辑式:Prolog,C我想学习的编程语......
  • Flutter/Dart第21天:Dart异步编程(Future/Stream)
    Dart官方文档:https://dart.dev/language/async重要说明:本博客基于Dart官网文档,但并不是简单的对官网进行翻译,在覆盖核心功能情况下,我会根据个人研发经验,加入自己的一些扩展问题和场景验证。Future处理我们有2种方式编写Future异步代码:使用async和wait关键字使用FutureAPI(ht......
  • 编程语言排名
    对于前20种热门语言命令式面向过程:CC++JavaC#JavaScriptPHPVisualBasicGoKotlinDelphi/ObjectPascalSwiftRubyRust面向对象:C++JavaC#JavaScriptPHPVisualBasicDelphi/ObjectPascalSwiftRubyKotlin声明式函数式:PythonSQLR逻辑式:Scratch(......
  • Python十道基础编程题
    1.输入日期,判断这一天是这一年的第几天importdatetimedefday_of_year():year=eval(input('请输入年份:'))month=eval(input('请输入月份:'))day=eval(input('请输入天:'))date1=datetime.date(year,month,day)date2=datetime.date......
  • 《Java编程思想第四版》学习笔记37--关于 TextField的ActionListener接收器
    //:TextNew.java//TextfieldswithJava1.1eventsimportjava.awt.*;importjava.awt.event.*;importjava.applet.*;publicclassTextNewextendsApplet{Buttonb1=newButton("GetText"),b2=newButton("SetText");TextFie......
  • 【教3妹学编程-算法题】数位和相等数对的最大和
    3妹:2哥,你有没有看到新闻“18岁父亲为4岁儿子落户现身亲子鉴定”2哥 :啥?18岁就当爹啦?3妹:确切的说是14岁好吧。2哥 :哎,想我30了,还是个单身狗。3妹:别急啊,2嫂肯定在某个地方等着你去娶她呢。又不是结婚越早越好。2哥:是啊,这孩子14岁当爹,也太早了。3妹:2哥,你找女朋友有什么条件没有......