概述
在函数式编程时,经常会用到filter/map/reduce函数,对于java语言,可以通过Stream轻松进行filter/map/reduce,同时在上一篇文章中,我也介绍了Stream另外一种更为简单的实现方式,既然学习go语言,我也希望可以像java一样比较简单的通过流的方式来进行filter/map/reduce
filter/map/reduce的基本实现方式
大家看到网上的文章,通常的实现方式如下
package main
import (
"fmt"
"strconv"
)
func main() {
arr := []string{"1", "2", "3", "4"}
r := Map(arr, func(it string) int {
i, _ := strconv.Atoi(it)
return i
})
fmt.Println(r)
r = Filter(r, func(it int) bool {
return it%2 == 0
})
fmt.Println(r)
c := Reduce(r, 0, func(init *int, it int) {
*init += it
}, func(i *int) int {
return *i * 3
})
fmt.Println(c)
}
func Map[T, R any](arr []T, f func(T) R) []R {
r := make([]R, len(arr))
for i, v := range arr {
r[i] = f(v)
}
return r
}
func Filter[T any](arr []T, test func(T) bool) []T {
r := make([]T, 0)
for _, v := range arr {
if test(v) {
r = append(r, v)
}
}
return r
}
func Reduce[T, I, R any](arr []T, init I, action func(*I, T), end func(*I) R) R {
for _, v := range arr {
action(&init, v)
}
return end(&init)
}
通过通用的filter/map/reduce实现,我写了一个将string slice转成int slice,让后过滤出偶数,最后进行汇总后再乘以3,实际的开发过程中,map和filter可能会发生多次
但是这样的实现存在一个问题,可以看到,每次map和filter之后,都会将slice中所有数据迭代后转出新的slice,这里会存两个问题
- 如果进行多次的map和filter,就会产生多个slice,性能有影响的同时对空间也有影响
- 假设有一个上亿条csv文件,我们像通过map进行每行数据转换,filter过滤掉部分不要的行之后写入新的csv文件,同时在文件尾部还要写入汇总信息,基于目前的filter/map/reduce行数,显然是无法实现的
java的Stream是一种流式计算的形式,slice中的数据会一条一条流过map,filter,最后被reduce,中间不需要临时的slice来存储数据
基于go的Stream实现
参考java版本的实现方案,依然先定义一个each函数
type Stream struct {
each func(func(*any))
}
func StreamOf(arr []any) *Stream {
stream := Stream{each: func(consumer func(*any)) {
for _, v := range arr {
consumer(&v)
}
}}
return &stream
}
接下来定义Map方法,方法实现上就是对原来的each函数进行套娃,套上转换逻辑
func (sm *Stream) Map(mapFunc func(*any) any) *Stream {
oldEach := sm.each
sm.each = func(f func(*any)) {
oldEach(func(a *any) {
aa := mapFunc(a)
f(&aa)
})
}
return sm
}
理解了Map方法,filter方法就比较好实现了
func (sm *Stream) Filter(testFunc func(*any) bool) *Stream {
oldEach := sm.each
sm.each = func(f func(*any)) {
oldEach(func(a *any) {
if testFunc(a) {
f(a)
}
})
}
return sm
}
接下来实现reduce方法,先实现个toSlice方法
func (sm *Stream) ToSlice() []any {
arr := make([]any, 0)
sm.each(func(a *any) {
arr = append(arr, *a)
})
return arr
}
再来个通用的reduce方法
func (sm *Stream) Reduce(init any, action func(*any, *any), end func(*any) any) any {
sm.each(func(it *any) {
action(&init, it)
})
return end(&init)
}
使用示例
package main
import "fmt"
func main() {
var arr []any = []any{1, 2, 3, 4}
r := StreamOf(arr).Map(func(it *any) any {
return (*it).(int) + 6
}).Map(func(it *any) any {
return (*it).(int) / 2
}).Filter(func(it *any) bool {
return (*it).(int)%2 != 0
}).toSlice()
fmt.Println(r)
}
//输出结果为:
[3 5]
进阶,实现FindFirst函数
findFirst就找到匹配的第一个数据,找到了就中断执行,会了FindFirst的实现,anyMatch,allMatch就比较好实现了。
增加stop标记
type Stream struct {
each func(func(*any))
//中断标记
stop *bool
}
func StreamOf(arr []any) *Stream {
s := false
stream := Stream{func(consumer func(*any)) {
for _, v := range arr {
//一旦中断,就跳出执行
if s {
break
}
consumer(&v)
}
}, &s}
return &stream
}
FindFirst实现
func (sm *Stream) FindFirst(testFunc func(*any) bool) *any {
var r *any = nil
oldEach := sm.each
sm.each = func(f func(*any)) {
oldEach(func(a *any) {
if testFunc(a) {
//找到了,中断循环
*(sm.stop) = true
r = a
}
})
}
sm.each(func(a *any) {})
return r
}
使用示例
package main
import "fmt"
func main() {
var arr []any = []any{1, 2, 3, 4}
r := StreamOf(arr).Map(func(it *any) any {
return (*it).(int) + 6
}).Map(func(it *any) any {
return (*it).(int) / 2
}).FindFirst(func(a *any) bool {
return *a == 4
})
fmt.Println(*r)
}
//输出:4
总结
go的实现思路和java的实现思路实际是一致的,差异点主要是语法上的不同。
作为go的初学者,从这个功能的实现上,我发现go的泛型支持没java那么方便,go的Stream实现我本来想支持泛型的,但是折腾了半天也没搞定就放弃了,如果大家有支持泛型的Stream方式,请告诉我一下