Go Experience Report: Generics for Functional Patterns

In mid-2017, I gave a talk at GopherCon called “Functional Programming in Go”. I presented some functional programming (FP) concepts that Gophers could use immediately to write code faster or clean up their code.

FP is possible in Go, just not obvious…

About 1/2 of the talk was theory, the other half was about patterns concepts that folks could use. About 1/4 of those were patterns that I thought would actually help, and the rest was stuff that was worth mentioning. You’d need code generation to really make them work for you (I shared some of these patterns in a Github repo for folks to “take home”).

This post is about how Generics would help bring more powerful FP patterns to Go, without relying on code generation.

I haven’t followed the guidelines for how to write experience reports exactly here because I believe these ideas are best expressed in a slightly different way.

An Example

Let’s start from the ideal API I’d like to have and work backward. Mapping over sequences is pretty common in pretty much all languages, so I’m going to focus on that use case for the rest of this post.

Here is, ideally, what I think the API should look like in Go:

ints := []int{1, 2, 3, 4}
incremented := ints.Map(func(i int) int { return i + 1 })
// incremented = []int{2, 3, 4, 5}

As you might know, Map approximately replaces a for loop. It helps you write pure code and is also fairly convenient to use, primarily because you don’t have to write the range yourself.

The above code looks ideal, but we’d have to make quite a few significant changes (i.e. something like automatic typeclass conversion) to the language in order to make it happen.

Also, although the above looks ideal, we’d have to add another special case to enable a new method (Map) on specific types (slices). That’s counter productive in my eyes. I’d rather focus on an experience report related to generics, as Russ Cox mentioned in his GopherCon 2017 Keynote. That’s what this article is about.

A More Realistic Example

I’ll propose a tweak to the above API slightly. It’s more practical in the language as-is and also handily focuses the discussion onto generics (instead of going into more functional patterns, which can be done in another article).

Here we wrap the []int in a “container” type, and then define the Map on that container:

incremented := WrapSlice(ints).Map(func(i int) int { return i + 1})

This new container partially implements type-class semantics in Go.

In today’s Go, there would be no way to make a single Wrap function work on any other type besides []int, again because we don’t have a mechanism for generic programming.

The closest you can get is either hand-writing or generating code to make something that looks like Wrap work for all the types you want it to work on.

What’s This Guy Got Against Code Generation?

The idea of, motivation behind, and technology behind code generation is good. I’ve written about its use before in my last experience report, and I’m not trying to bash it here or in the other article.

It may sound like that, though, because I firmly believe that code generation is a tool that should be chosen for the right job, just like everything else. And code generation is absolutely the wrong tool for this job.

Here’s the big issue with it, illustrated by example. Assume you want to generate code to handle []ints, []strings and MyCustomTypes. There would be no way for the generator to give you the same Wrap function for all your types in today’s Go. Instead, the generator would produces code that gives us this API:

myStrings := []string{...}
WrapStringSlice(myStrings).Map(func(s string) string {
return "hello " + s
})
myInts := []int{...}
WrapIntSlice(myInts).Map(func(i int) int {
return i + 1
})
myCustomTypes := []MyCustomType{...}
WrapMyCustomTypeSlice(myCustomTypes).Map(func(m MyCustomType) MyCustomType {
return m
})

So we have one Wrap function per type. We get compatibility for all our types, but we still don’t have an API to write generic code against all our types.

What Next

Obviously I’m writing about adding generics to Go, but “generics” can mean a lot of things. I’m illustrating here exactly what I want a generics API to look like to enable this FP pattern. You can extrapolate this API to other FP patterns.

To start, I want to be able to call Map on a []T or a map[T]U (T and U are arbitrary types) and be able to transform the values into other slices or maps ( []A and map[B]C). As with my last post, I am not going to invent a syntax for generics in Go here, I’m only going to show what I would want it to look like. I may write a follow-up post to propose a syntax.

WrapSlice

I showed what I want WrapSlice and Map to look like above, but that was a simple example. The powerful feature of Map is that you can convert slices from one type to another (i.e. T1 => T2). That functionality looks the same as the above example except for the function signature passed to Map (notice the parameter and return value are different types):

ints := []int{1, 2, 3, 4}
strs := WrapSlice(ints).Map(func(i int) string {
    return strconv.Itoa(i*2)
})
// var strs []string = []string{"2", "4", "6", "8"}
bites := WrapSlice(strs).Map(func(s string) []byte {
    return []byte(s)
})
// var bites [][]byte = []byte{
// []byte{50},
// []byte{52},
// []byte{54},
// []byte{56},
// }

Here we’ve converted a slice of ints to a slice of strings, and then that slice of strings into a slice of byte slices.

WrapMap

WrapMap is logically similar to WrapSlice, except this time we’re converting key/value pairs. For example, this is what map[string]int to the equivalent map[int]string looks like:

m := map[string]int{
"1": 1,
"2": 2,
"3": 3,
}
converted := WrapMap(m).Map(func(k string, v int) (int, string) {
newKey := strconv.Itoa(v)
newVal, _ := strconv.Atoi(k)
return newKey, newVal
})
// var converted map[int]string = { 1: "1", 2: "2", 3: "3" }

Conclusion

The important piece to take away from the Wrap* / Map examples is that they work on all T and U types. They can be written outside of the language code, and instead could go into the standard library, a third party “FP” package, or even an ecosystem of them.

Finally, if you can’t see where generics would be used from the above examples, I’ll explain here. WrapList is parameterized by the type of the list elements (i.e. T in []T), and WrapMap is parameterized by the type of the key and the type of the value (i.e. map[T]U).

Then, Map is parameterized by a new list type in the case of mapping over lists (i.e. Map[U]) and Map is parameterized by the new key and new value type in the case of mapping over maps (i.e. Map[X, Y]).

Like I said before, I didn’t formally specify any syntax in here, but I did use the ‘bracket’ syntax — which I prefer — to illustrate type parameters. Without committing to anything — generics syntax specification is a big topic! — I am planning to write a more detailed proposal for syntax. Until then…

Keep on rockin’, Gophers!


If you’re interested in learning more about Go, check out the Go in 5 Minutes screencasts. I’d also love a subscribe too!