Achieving type generic functions in Go, without using reflections

Of course we all know that Go does not support generic types, and complaining about it would be a fruitless act (for now). Aside from its design’s pros/cons, I must admit that there are situations that we need functions which are almost same but only differ in types. The larger our programs become, the more likely we want such functions. For this need, we have to define one function per one type, as sort.IntSlice and sort.StringSlice.

There are some ways to attack this problem, including code generation. Lucky to hear, as of Go 1.4 go generate was introduced, which encourages code generation, so I decided to write a tool that achieves generic functions following these strategies below:

  1. Use no reflections, do code generation.
  2. The source code before code generation should be well compiled.
  3. The calling code of the generic function should not be changed by code generation. That is, we use only one function for different types and don’t generate functions for each types.

Suppose we want a function like:

// No so useful.
func keys(m map[string]T) []string {
keys := make([]string, 0, len(m))
for key := range m {
keys = append(keys, key)
}
return keys
}

This function is not so useful as it does not accept concrete types like map[string]bool, because bool does not equal to T. The parameter type should be interface{} to receive various types of arguments. So, to begin with, let’s modify the function slightly:

func keys(m interface{}) []string {
switch m := m.(type) {
case map[string]T:
keys := make([]string, 0, len(m))
for key := range m {
keys = append(keys, key)
}
return keys
default:
panic(fmt.Sprintf("unexpected type: %T", m))
}
}

Now we’ve got a function that compiles well. Of course it fails on run-time as it is, we can add a new case clauses for each concrete types we want to handle, replacing “T” with concrete types such as “bool”. I made a command which does the work, named tsgen. tsgen is a Go code generator focused on type switches.

Installation/How it works

Run below to install tsgen.

go get github.com/motemen/go-typeswitch-gen/cmd/tsgen

“tsgen expand” takes a file and searches for type switches on interface{} and a template case clause. It then analyses the program to assume what concrete types come there, match them to the template case type to generate a case clause for concrete types.

# -w to rewrite the file, otherwise prints out to stdout
tsgen -w expand keys.go

For example, the result may look like:

func keys(m interface{}) []string {
switch m := m.(type) {
case map[string]int:
keys := make([]string, 0, len(m))
for key := range m {
keys = append(keys, key)
}
return keys
case map[string]bool:
keys := make([]string, 0, len(m))
for key := range m {
keys = append(keys, key)
}
return keys
case map[string]T:
keys := make([]string, 0, len(m))
for key := range m {
keys = append(keys, key)
}
return keys
default:
panic(fmt.Sprintf("unexpected type: %T", m))
}
}

Now keys(map[string]int{}) and keys(map[string]bool{}) are valid and work as expected. You can see complete examples under _example directory.

go get github.com/motemen/go-typeswitch-gen/cmd/tsgen
tsgen expand $(go list -f '{{.Dir}}' github.com/motemen/go-typeswitch-gen)/_example/keys/keys.go

What will be a type placeholder?

tsgen considers a type that is an interface{} with name consist of only uppercase letters and numbers as a type placeholder.

type T1 interface{}

A type whose declaration is commented with `+tsgen typevar` is also a placeholder.

// +tsgen typevar
type NumT float64

With this kind of placeholders, you can apply binary operators on values of generic types.

var sum NumT
for _, x := range a {
sum = sum + x
}

TODO and Caveats

  • We cannot achieve generic return types by tsgen as it does not modify caller side code.
  • tsgen does not automatically add imports if new case clauses need one.
  • Comments inside type switches will be misplaced after tsgen.

tsgen provides more functionalities for type switch statements: clause sorting and scaffolding. For more information, please check out the repository!

Web engineer at Kyoto, loves #Git #Golang #Vim

Web engineer at Kyoto, loves #Git #Golang #Vim