背景 函数式编程(functional programming / fp)作为一种编程范式,具有无状态、无副作用、并发友好、抽象程度高等优点。目前流行的编程语言(c++、python、rust)都或多或少地引入了函数式特性,但在同作为流行语言的 golang 中却少有讨论。
究其原因,大部分的抱怨golang 函数式编程简述[1] 、gophercon 2020: dylan meeus - functional programming with go[2]集中于 go 缺乏对泛型的支持,难以写出类型间通用的函数。代码生成只能解决一部分已知类型的处理,且无法应对类型组合导致复杂度(比如实现一个通用的 typea → typeb 的 map 函数)。
有关泛型的提案 spec: add generic programming using type parameters #43651[3] 已经被 go 团队接受,并计划在 2022 年初发布支持泛型的 go 1.18,现在 golang/go 仓库的 master 分支已经支持泛型。
this design has been proposed and accepted as a future language change. we currently expect that this change will be available in the go 1.18 release in early 2022. type parameters proposal[4]
基于这个重大特性,我们有理由重新看看,函数式特性在 go 泛型的加持下,能否变得比以往更加实用。
概述 这篇文章里,我们会尝试用 go 的泛型循序渐进地实现一些常见的函数式特性,从而探索 go 泛型的优势和不足。
除非额外说明(例如注释中的 // invalid code!!!),文章里的代码都是可以运行的(为了缩减篇幅,部分删去了 package main 声明和 main 函数,请自行添加)。你可以自行 从源码编译[5] 一个 master 版本的 go 来提前体验 go 的泛型,或者用 the go2go playground[6] 提供的在线编译器运行单个文件。
泛型语法 提案的 #very high level overview[7] 一节中描述了为泛型而添加的新语法,这里简单描述一下阅读本文所需要的语法:
函数名后可以附带一个方括号,包含了该函数涉及的类型参数(type paramters)的列表:func f[t any](p t t any) { ... }
这些类型参数可以在函数参数和函数体中(作为类型)被使用
自定义类型也可以有类型参数列表:type m[t any] []t
每个类型参数对应一个类型约束,上述的 any 就是预定义的匹配任意类型的约束
类型约束在语法上以 interface 的形式存在,在 interface 中嵌入类型 t 可以表示这个类型必须是 t:
type integer1 interface { int} 嵌入单个类型意义不大,我们可以用 | 来描述类型的 union:
type integer2 interface { int | int8 | int16 | int32 | int64} ~t 语法可以表示该类型的「基础类型」是 t,比如说我们的自定义类型 type myint int 不满足上述的 integer1 约束,但满足以下的约束:
type integer3 interface { ~int} 提示
「基础类型」在提案中为 “underlying type”,目前尚无权威翻译,在本文中使用仅为方便描述。
高阶函数 在函数式编程语言中, 高阶函数[8] (higher-order function)是一个重要的特性。高阶函数是至少满足下列一个条件的函数:
接受一个或多个函数作为输入 输出一个函数 golang 支持闭包,所以实现高阶函数毫无问题:
func foo(bar func() string) func() string { return func() string { return foo + + bar() }}func main() { bar := func() string { return bar } foobar := foo(bar) fmt.println(foobar())}// output:// foo bar filter 操作是高阶函数的经典应用,它接受一个函数 f(func (t) bool)和一个线性表 l([] t),对 l 中的每个元素应用函数 f,如结果为 true,则将该元素加入新的线性表里,否则丢弃该元素,最后返回新的线性表。
根据上面的泛型语法,我们可以很容易地写出一个简单的 filter 函数:
func filter[t any](f func(t t any) bool, src []t) []t { var dst []t for _, v := range src { if f(v) { dst = append(dst, v) } } return dst}func main() { src := []int{-2, -1, -0, 1, 2} dst := filter(func(v int) bool { return v >= 0 }, src) fmt.println(dst)}// output:// [0 1 2] 代码生成之困 在 1.17 或者更早前的 go 版本中,要实现通用的 filter 函数有两种方式:
使用 interface{} 配合反射,牺牲一定程度的类型安全和运行效率 为不同数据类型实现不同的 filter 变种,例如 filterint、filterstring 等,缺点在于冗余度高,维护难度大 方式 2 的缺点可以通过代码生成规避,具体来说就使用相同的一份模版,以数据类型为变量生成不同的实现。我们在 golang 内部可以看到不少 代码生成的例子[9] 。
那么,有了代码生成,我们是不是就不需要泛型了呢?
答案是否定的:
代码生成只能针对已知的类型生成代码,明明这份模版对 float64 也有效,但作者只生成了处理 int 的版本,我们作为用户无能为力(用 interface{} 同理,我们能使用什么类型,取决于作者列出了多少个 type switch 的 cases)
而在泛型里,新的类型约束语法可以统一地处理「基础类型」相同的所有类型:
type signed interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~float32 | ~float64 | ~complex64 | ~complex128}func neg[t signed](n t t signed) t { return -n}func main() { type myint int fmt.println(neg(1)) fmt.println(neg(1.1)) fmt.println(neg(myint(1)))}// output:// -1// -1.1// -1 代码生成难以应对需要类型组合的场景,我们来看另一个高阶函数 map:接受一个函数 f(func (t1) t2)和一个线性表 l1([]t1),对 l1 中的每个元素应用函数 f,返回的结果组成新的线性表 l2([]t2)
如果使用代码生成的话,为了避免命名冲突,我们不得不写出 mapintint、mapintuint、mapintstring 这样的奇怪名字,而且由于类型的组合,代码生成的量将大大膨胀。
我们可以发现在现有的支持 fp 特性的 go library 里:
如果使用泛型的话,只需要定义这样的签名就好了:
func map[t1, t2 any](f func(t1 t1, t2 any) t2, src []t1) []t2 有的( hasgo[10] )选择将 map 实现成了闭合运算([]t → []t),牺牲了表达能力 有的( functional-go[11] )强行用代码生成导致接口数目爆炸 有的( fpgo[12] )选择牺牲类型安全用 interface{} 实现 无糖的泛型 go 的语法在一众的编程语言里绝对算不上简洁优雅。在官网上看到操作 channel 时 x >= 0) [-2, -1, 0, 1, 2]-- output:-- [0,1,2] 而在 golang 里,函数的类型签名不可省略,无论高阶函数要求何种签名,调用者在构造闭包的时候总是要完完整整地将其照抄一遍golang 函数式编程简述[13]
func foo(bar func(a int, b float64, c string) string) func() string { return func() string { return bar(1, 1.0, ) }}func main() { foobar := foo(func(_ int, _ float64, c string) string { return c }) foobar()} 这个问题可以归结于 go 团队为了保持所谓的「大道至简」,而对类型推导这样提升效率降低冗余的特性的忽视(泛型的姗姗来迟又何尝不是如此呢?)。proposal: go 2: lightweight anonymous function syntax #21498[14] 提出了一个简化闭包调用语法的提案,但即使该提案被 accept,我们最快也只能在 go 2 里见到它了。
方法类型参数 链式调用[15] (method chaining)是一种调用函数的语法,每个调用都会返回一个对象,紧接着又可以调用该对象关联的方法,该方法同样也返回一个对象。链式调用能显著地消除调用的嵌套,可读性好。我们熟悉的 gorm 的 api 里就大量使用了链式调用:
db.where(name = ?, jinzhu).where(age = ?, 18).first(&user) 在函数式编程中,每个高阶函数往往只实现了简单的功能,通过它们的组合实现复杂的数据操纵。
在无法使用链式调用的情况下,高阶函数的互相组合是这样子的(这仅仅是两层的嵌套):
map(func(v int) int { return v + 1 }, filter(func(v int) bool { return v >= 0 }, []int{-2, -1, -0, 1, 2})) 如果用链式调用呢?我们继续沿用前面的 filter ,改成以下形式:
type list[t any] []tfunc (l list[t]) filter(f func(t) bool) list[t] { var dst []t for _, v := range l { if f(v) { dst = append(dst, v) } } return list[t](dst t)}func main() { l := list[int]([]int{-2, -1, -0, 1, 2} int). filter(func(v int) bool { return v >= 0 }). filter(func(v int) bool { return v -2 }, l)l = filter(func(v int) bool { return v -2、if v < 2,最后执行 v + 1,放入新的 []int 中,空间复杂度依然是 o(n),但毫无疑问地我们只使用了一个 `[]int``。
泛型的引入对惰性求值的好处有限,大致和前文所述一致,但至少我们可以定义类型通用的 接口了:
// 一个适用于线性结构的迭代器接口type iter[t any] interface{ next() (t, bool) }// 用于将任意 slice 包装成 iter[t]type sliceiter[t any] struct { i int s []t}func iterofslice[t any](s []t t any) iter[t] { return &sliceiter[t]{s: s}}func (i *sliceiter[t]) next() (v t, ok bool) { if ok = i.i -2 }, i)i = filter(func(v int) bool { return v -2 })r := f1.partial(iterofslice([]int{-2, -1, -0, 1, 2}))fmt.println(list(r))// output:// [-1 0 1 2] 类型参数推导 我们勉强实现了 partial application,可是把 filter 转换为 funcwith2args 的过程太过繁琐,在上面的例子中,我们把类型参数完整地指定了一遍,是不是重新感受到了 闭包语法[24] 带给你的无奈?
这一次我们并非无能为力,提案中的 #type inference[25] 一节描述了对类型参数推导的支持情况。上例的转换毫无歧义,那我们把类型参数去掉:
// invalid code!!!f2 := funcwith2args(filter[int]) 编译器如是抱怨:
cannot use generic type funcwith2args without instantiation
提案里的类型参数推导仅针对函数调用,funcwith2args(xxx) 虽然看起来像是函数调用语法,但其实是一个类型的实例化,针对类型实例化的参数类型推导( #type inference for composite literals[26] )还是一个待定的 feature。
如果我们写一个函数来实例化这个对象呢?很遗憾,做不到:我们用什么表示入参呢?只能写出这样「听君一席话,如听一席话」的函数:
func cast[a1, a2, r any](f funcwith2args[a1, a2, r] a1, a2, r any) funcwith2args[a1, a2, r] { return f} 但是它能工作!当我们直接传入 filter 的时候,编译器会帮我们隐式地转换成一个 funcwith2args[func(int) bool, iter[int], iter[int]]!同时因为函数类型参数推导的存在,我们不需要指定任何的类型参数了:
f2 := cast(filter[int])f1 := f2.partial(func(v int) bool { return v > -2 })r := f1.partial(iterofslice([]int{-2, -1, -0, 1, 2}))fmt.println(list(r))// output:// [-1 0 1 2] 可变类型参数 funcwith1args 、funcwith2args 这些名字让我们有些恍惚,仿佛回到了代码生成的时代。为了处理更多的参数,我们还得写 funcwith3args、funcwith4args… 吗?
是的, #omissions[27] 一节提到:go 的泛型不支持可变数目的类型参数:
no variadic type parameters. there is no support for variadic type parameters, which would permit writing a single generic function that takes different numbers of both type parameters and regular parameters.
对应到函数签名,我们也没有语法来声明拥有不同类型的可变参数。
类型系统 众多函数式特性的实现依赖于一个强大类型系统,go 的类型系统显然不足以胜任,作者不是专业人士,这里我们不讨论其他语言里让人羡慕的类型类(type class)、代数数据类型(algebraic data type),只讨论在 go 语言中引入泛型之后,我们的类型系统有哪些水土不服的地方。
提示
其实上文的大部分问题都和类型系统息息相关,case by case 的话我们可以列出非常多的问题,因此以下只展示明显不合理那部分。
编译期类型判断 当我们在写一段泛型代码里的时候,有时候会需要根据 t 实际上的类型决定接下来的流程,可 go 的完全没有提供在编译期操作类型的能力。运行期的 workaround 当然有,怎么做呢:将 t 转化为 interface{},然后做一次 type assertion:
func foo[t any](n t t any) { if _, ok := (interface{})(n).(int); ok { // do sth... }} 无法辨认「基础类型」 我们在 代码生成之困[28] 提到过,在类型约束中可以用 ~t 的语法约束所有 基础类型为 t 的类型,这是 go 在语法层面上首次暴露出「基础类型」的概念,在之前我们只能通过 reflect.(value).kind 获取。而在 type assertion 和 type switch 里并没有对应的语法处理「基础类型」:
type int interface { ~int | ~uint}func issigned[t int](n t t int) { switch (interface{})(n).(type) { case int: fmt.println(signed) default: fmt.println(unsigned) }}func main() { type myint int issigned(1) issigned(myint(1))}// output:// signed// unsigned 乍一看很合理,myint 确实不是 int。那我们要如何在函数不了解 myint 的情况下把它当 int 处理呢?答案是还不能:#identifying the matched predeclared type[29] 表示这是个未决的问题,需要在后续的版本中讨论新语法。总之,在 1.18 中,我们是见不到它了。
类型约束不可用于 type assertion 一个直观的想法是单独定义一个 signed 约束,然后判断 t 是否满足 signed:
type signed interface { ~int}func issigned[t int](n t t int) { if _, ok := (interface{})(n).(signed); ok { fmt.println(signed) } else { fmt.println(unsigned) }} 但很可惜,类型约束不能用于 type assertion/switch,编译器报错如下:
interface contains type constraints
尽管让类型约束用于 type assertion 可能会引入额外的问题,但牺牲这个支持让 go 的类型表达能力大大地打了折扣。
总结 函数式编程的特性不止于此,代数数据类型、引用透明(referential transparency)等在本文中都未能覆盖到。总得来说,go 泛型的引入:
PCB板材有铅喷锡和无铅喷锡谁更好
谷歌又推新硬件:“白板”也可以很高科技
Freescale MCF5441x系列产品的主要功能特性及应用电路
kernel panic流程分析
野外作战时既能保护眼睛又可实时获取信息的重要装备——AR战术眼镜
Golang函数式编程简述
传感器巨头,终止激光雷达开发计划!
华为的救赎从史上最强大的Mate手机开始
三星s8自信爆棚,业务主管:销量超过老旗舰没毛病!
智能家居中有哪一些控制的方式
什么是COP?能为开发者带来哪些优势?如何选择最佳COP器件?
【分享】什么是串口通信?串口常见通信问题如何解决?
中国移动推动“网络+云+资源”深度融合,满足不同客户按单点菜的需求
机器人浮动主轴打磨去毛刺工具及应用案例
腾讯云区块链服务TBaaS架构介绍
云南联通混改模式是否有望在全国范围内进一步推广?
加速度计与MEMS明日之星
C语言函数参数介绍
软包电池优劣势有哪些?
网络攻击之CC攻击及相关防御方案