Golang的流式代码 - 0x46


Go 1.18 刚刚发布,这意味着 Go 现在正式支持泛型。出于好奇,我决定研究创建一个实现类似于 Java 流的库。我的简单实现的目标是支持使用两个操作处理切片的元素:映射和过滤。
如果您只想查看代码,则可以在此处找到存储库
我没有在本文中展示实现,坦率地说,它并不那么有趣。
我们将只看使用这个库产生的代码。这样我们就可以评估使用类似的库是否会使 Go 程序更具可读性。
  
过滤
在本节中,我们将尝试过滤一个整数切片,在结果切片中只留下偶数。首先让我们看一下使用带有for循环的标准方法执行此操作的一段代码:

func OnlyEven(slice []int) []int {
        var result []int

        for _, element := range slice {
            if element%2 == 0 {
                    result = append(result, element)
            }
        }

        return result
}

我相信大多数人在使用 Go 编程时编写了数百个类似的函数。这样简单的函数编写起来非常重复,但很容易识别,并且通常与我刚刚介绍的函数一样简单。
现在让我们看看使用我编写的库实现的相同功能:
func OnlyEven(slice []int) []int {
    return streams.New(slice).
        Filter(onlyEven).
        Collect()
}

func onlyEven(v int) bool {
    return v%2 == 0
}

为了便于阅读,我用命名函数替换了匿名函数。我相信这段代码相对容易阅读,但我很难判断它是否比之前的函数更具可读性。然而,它似乎写起来更快,并且包含的​​样板较少,这可能会损害可读性。
总的来说,我相信for在某些情况下,过滤器调用链可能比循环内的许多语句更具可读性。话虽如此,我认为普通for循环不会令人讨厌或难以阅读,因此很难判断这是否解决了任何实际问题。
 
过滤和映射
现在让我们试着让这个例子更复杂一些。首先我们将过滤这些值,然后将它们从 映射int到string,然后过滤掉超过一位数的值。为了简单len起见,我们将只使用类似RuneCountInString.
同样,首先让我们尝试一个传统的for循环:

func OnlyEvenAsStrings(slice []int) []string {
        var result []int

        for _, element := range slice {
            if element%2 != 0 {
                continue
            }

            s := strconv.Itoa(element)
            if len(s) > 1 {
                continue
            }

            result = append(result, s)
        }

        return result

如您所见,我试图以一种避免嵌套条件语句的方式构造代码。尽管可以通过多种方式编写此函数,但我认为这种方法使控制流更易于遵循。所有条件语句都可以清楚地识别为过滤元素的条件。
现在让我们尝试对我的库做同样的事情:
func OnlyEvenAsStrings(slice []int) []string {
    return streams.Map(
        streams.New(slice).Filter(onlyEven),
        strconv.Itoa,
    ).
        Filter(onlyOneByte).
        Collect()
}

func onlyEven(v int) bool {
    return v%2 == 0
}

func onlyOneByte(v string) bool {
    return len(v) == 1
}

不幸的是,正如您所看到的,代码突然变得不那么可读了。在我看来,函数应该是这样的:
func OnlyEvenAsStrings(slice []int) []string {
    return streams.New(slice).
        Filter(onlyEven).
        Map(strconv.Itoa).
        Filter(onlyOneByte).
        Collect()
}

不幸的是,以Go中泛型的工作方式,目前这是不可能的。
真实的例子看起来很糟糕,因为方法不能用额外的类型参数进行参数化。
由于这个原因,我不得不使用一个顶层函数。这是很不幸的,因为我认为我的理想化例子会更有意义。
目前产生的代码在可读性方面肯定是完全失败的。
 
关于性能的简短说明
出于好奇,我对第一节中的过滤函数进行了基准测试。一个简单的循环的性能比试图用我的库来执行同样的任务要好。虽然我的实现可能过于天真,但我不认为这与我的代码有必然联系。我假设这是由编译器在使用简单循环时进行的各种优化造成的。因此,一个更复杂的实现很可能总是失败。