函数式编程是很多语言正在支持或已经支持的日渐流行的编程范式。Go 已经支持了其中一部分的特性,比如头等函数和更高阶功能的支持,使函数式编程成为可能。
Go 缺失的一个关键特性是泛型。缺少这个特性,Go 的函数库和应用不得不从下面的两种方法中选择一种:类型安全 + 特定使用场景或类型不安全 + 未知使用场景。在 2022 年初即将发布的 Go 1.18 版本,泛型将被加进来,从而使 Go 支持新型的函数式编程形式。
在本篇文章中,我将介绍一些函数式编程的背景,Go 函数式编程的现状调查,并讨论 Go 1.18 计划的特性以及如何将它们用于函数式编程。
维基百科中定义的函数式编程是:
通过应用组合函数的编程范式。
更具体的说,函数式编程有以下几个关键特征:
对于函数式编程更详细的信息,可以参考这两篇有详细描述例子的文章:函数式编程是什么?和函数式的 Go
函数式编程是让开发者提升代码质量的一些模式。这些质量提升的模式并非函数式编程独有,而是一些 “免费” 的优势。
正如多数开发者从经验中学到的,如 Robert C. Martin 在代码整洁之道中所说:
确实,相对于写代码,花费在读代码上的时间超过 10 倍。为了写出新代码,我们一直在读旧代码。…[因此,] 让代码更易读,可以让代码更易写。
根据团队的经验或学习函数式编程的意愿,这些优势会产生很大的影响。相反,对于缺乏经验和足够时间投入学习的团队,或维护大型的代码仓库时,函数式编程将会产生相反的作用,上下文切换的引入或显著的重构工作将无法产生相应的价值。
Go 不是一门函数语言,但确实提供了一些允许函数式编程的特性。有大量的 Go 开源库提供函数特性。我们将会讨论泛型的缺失导致这些库只能折衷选择。
函数式编程的语言支持包括一系列从仅支持函数范式(比如 Haskell)到多范式和头等函数的支持(比如 Scale、Elixir),还包括多范式和部分支持(如 Javascript、Go)。在后面的语言中,函数式编程的支持一般是通过使用社区创建的库,它们复制了前面两个语言的部分或全部的标准库的特性。
属于后一种类别的 Go 要使用函数式编程需要下面这些特性:
† 将在 Go 1.18 中可用(2022 年初)
在 Go 生态中,有大量函数式编程的库,区别在于流行度、特性和工效。由于缺少泛型,它们全部只能从下面两种选择中取一个:
func UniqString(data []string) []string
和 func UniqInt(data []int) []int
都是类型安全的,但只能应用在预定义的类型func Uniq(data interface{}) interface{}
这两种设计选择显示了两种相似的不吸引人的选项:有限的使用或运行时崩溃的风险。最简单也许最常见的选择是不使用 Go 的函数式编程库,坚持指令式的风格。
在2021年3月19日,泛型的设计提案通过并定为 Go 1.18 发行版的一部分。有了泛型之后,函数式编程库就不再需要在可用性和类型安全之间进行折衷。
Go 开发组发布了一个 go 1.18 游乐场,便于大家尝鲜泛型。同时也有一个实验性的编译器,在 go 代码仓库的一个分支上实现了泛型特性的最小集合。这两个都是在 Go 1.18 上尝鲜泛型的不错选择。
在前面说到的那个 unique 函数使用了两种可能的设计方法。有了泛型,它可以重写为 func Uniq[T](data []T) []T
,并可以使用任意类型来调用,比如 Uniq[string any](data []string) []string
或 Uniq[MyStruct any](data []MyStruct) []MyStruct
。为了进一步阐述这个概念,下面是一个具体的例子,展示了在 Go 1.18 中如何使用函数式单元来解决实际问题。
一个在网络世界常见的案例是 HTTP 的请求响应,其中 API 接口返回的 JSON 数据一般会被消费应用转换为一些有用的结构。
考虑下这个从 API 返回用户、得分和朋友信息的响应:
[
{
"id": "6096abc445dbb831decde62f",
"index": 0,
"isActive": true,
"isVerified": false,
"user": {
"points": 7521,
"name": {
"first": "Ramirez",
"last": "Gillespie"
},
"friends": [
{
"id": "6096abc46573cedd17fb0201",
"name": "Crawford Arnold"
},
...
],
"company": "SEALOUD"
},
"level": "gold",
"email": "ramirez.gillespie@sealoud.com",
"text": "Consequat pariatur aliquip pariatur mollit mollit cillum sint. Elit est nisi velit cillum. Ex mollit dolor qui velit Lorem proident ullamco magna velit nulla qui. Elit duis non ad laborum ullamco irure nulla culpa. Proident culpa esse deserunt minim sint nisi duis culpa nostrud in incididunt ad. Amet qui laborum deserunt proident adipisicing exercitation quis.",
"created_at": "Saturday, August 3, 2019 8:12 AM",
"greeting": "Hello, Ramirez! You have 9 unread messages.",
"favoriteFruit": "banana"
},
...
]
假设目标是获取各个等级的高分用户。我们将看下函数式和指令式风格的样子。
// imperative
func getTopUsers(posts []Post) []UserLevelPoints {
postsByLevel := map[string]Post{}
userLevelPoints := make([]UserLevelPoints, 0)
for _, post := range posts {
// Set post for group when group does not already exist
if _, ok := postsByLevel[post.Level]; !ok {
postsByLevel[post.Level] = post
continue
}
// Replace post for group if points are higher for current post
if postsByLevel[post.Level].User.Points < post.User.Points {
postsByLevel[post.Level] = post
}
}
// Summarize user from post
for _, post := range postsByLevel {
userLevelPoints = append(userLevelPoints, UserLevelPoints{
FirstName: post.User.Name.First,
LastName: post.User.Name.Last,
Level: post.Level,
Points: post.User.Points,
FriendCount: len(post.User.Friends),
})
}
return userLevelPoints
}
posts, _ := getPosts("data.json")
topUsers := getTopUsers(posts)
fmt.Printf("%+v\n", topUsers)
// [{FirstName:Ferguson LastName:Bryant Level:gold Points:9294 FriendCount:3} {FirstName:Ava LastName:Becker Level:silver Points:9797 FriendCount:2} {FirstName:Hahn LastName:Olsen Level:bronze Points:9534 FriendCount:2}]
样例的完整代码
// functional
var getTopUser = Compose3[[]Post, []Post, Post, UserLevelPoints](
// Sort users by points
SortBy(func (prevPost Post, nextPost Post) bool {
return prevPost.User.Points > nextPost.User.Points
}),
// Get top user by points
Head[Post],
// Summarize user from post
func(post Post) UserLevelPoints {
return UserLevelPoints{
FirstName: post.User.Name.First,
LastName: post.User.Name.Last,
Level: post.Level,
Points: post.User.Points,
FriendCount: len(post.User.Friends),
}
},
)
var getTopUsers = Compose3[[]Post, map[string][]Post, [][]Post, []UserLevelPoints](
// Group posts by level
GroupBy(func (v Post) string { return v.Level }),
// Covert map to values only
Values[[]Post, string],
// Iterate over each nested group of posts
Map(getTopUser),
)
posts, _ := getPosts("data.json")
topUsers := getTopUsers(posts)
fmt.Printf("%+v\n", topUsers)
// [{FirstName:Ferguson LastName:Bryant Level:gold Points:9294 FriendCount:3} {FirstName:Ava LastName:Becker Level:silver Points:9797 FriendCount:2} {FirstName:Hahn LastName:Olsen Level:bronze Points:9534 FriendCount:2}]
样例的完整代码
从上面的样例中可以看出一些特性:
在上面的例子中,两个使用场景使用了 go2go 编译器和一个叫做 pneumatic 的 Go 1.18 库,它提供了与Ramda (JavaScript), Elixir 标准库以及其他相似的常见函数式单元。鉴于 go2go 编译器有限的特性集,在本文发布时 pneumatic 只能用于实验目的,但从长期看,随着 Go 1.18 编译器的逐渐成熟,它会包含常见的函数式 Go 库。设置 pneumatic 和使用 Go 1.18 进行函数式编程的指导参见 pneumatic readme。
Go 增加泛型将会支持新型的方案、方法和范式,从而成为众多支持函数式编程的语言之一。随着函数式编程的逐渐流行,函数式编程的支持也会越来越好,从而有机会吸引那些现在还没考虑学习 Go 的开发者并让社区持续发展——这是在我看来比较积极的一面。非常期待看到在后续支持泛型之后和它带来新的解决方法后,Go 社区和生态将会发展成什么样。
go-funk [2.5k stars, type-safe or generic, active]
go-underscore [1.2k stars, generic, abandoned]
gubrak [336 stars, generic, active]
fpGo [167 stars, generic, active]
functional-go [92 stars, type-safe, active]
Go 泛型的过去、现在和将来
原文地址:https://ani.dev/2021/05/25/functional-programming-in-go-with-generics/
原文作者:Ani Channarasappa
本文永久链接:https://github.com/gocn/translator/blob/master/2021/w24_functional_programming_in_go_with_generics.md
译者:cvley
别忘了还有 Gopher China 2021 大会在文末等着你哦~
想和各位技术大佬们同台见面嘛?
那就赶快点击下方「阅读原文」报名参加呀!