Playing with Generics in Go

2020 has been full of surprises. It’s been some years since the first implementation of generics in Go, and I can’t believe I wrote this title and it’s not an April Fools prank.

Yesterday an article named The Next Step for Generics was published on the Go Blog with the current draft for generics in Go. They also released a version of The Go Playground which runs this current draft.

The first thing I tried to write was an implementation of Map that would receive a slice of strings and apply strings.ToUpper to every element of it [play]:

package main

import (
	"fmt"
	"strings"
)

func Map(type T)(s []T, fn func(t T) T) []T {
	ret := make([]T, len(s))

	for i, input := range s {
		ret[i] = fn(input)
	}

	return ret
}

func main() {
	var (
		slice        = []string{"This\n", "is\n", "some\n", "string"}
		changedSlice = Map(slice, strings.ToUpper)
	)

	fmt.Printf("%v\n", changedSlice)
}

The output is:

[THIS
IS
SOME
STRING]

What about mapping a slice of ints? Same thing, but after all these years copying functions and changing their types, I wanted to believe I could use the same Map function to apply another function using different parameters [play].

package main

import (
	"fmt"
)

func Map(type T)(s []T, fn func(t T) T) []T {
	ret := make([]T, len(s))

	for i, input := range s {
		ret[i] = fn(input)
	}

	return ret
}

func main() {
	var (
		numSlice        = []int{1, 2, 3, 4, 5, 6}
		changedNumSlice = Map(numSlice, func(i int) int { return i * 2 })
	)

	fmt.Printf("%v\n", changedNumSlice)
}

And the output is:

[2 4 6 8 10 12]

What if I want to create a Map function that converts a type T1 to a type T2? In this example, we’ll convert an int to an int64, nothing really fancy. I also added a ForEach implementation to print the slice [play].

package main

import (
	"fmt"
)

func Map(type T1, T2)(s []T1, fn func(t T1) T2) []T2 {
	ret := make([]T2, len(s))

	for i, input := range s {
		ret[i] = fn(input)
	}

	return ret
}

func ForEach(type T)(s []T, fn func(t T)) {
	for _, input := range s {
		fn(input)
	}

}

func main() {
	var (
		toInt64 = func(i int) int64 { return int64(i) }
		print = func(i int64) { fmt.Printf("%d is an int64\n", i) }

		numSlice        = []int{1, 2, 3, 4, 5, 6}
		changedNumSlice = Map(numSlice, toInt64)
	)

	ForEach(changedNumSlice, print)
}

Again, the output:

1 is an int64
2 is an int64
3 is an int64
4 is an int64
5 is an int64
6 is an int64

Well, a long time ago I used to have dozens of implementations of Contains to check if a slice contained some element. So here I tried to re-implement them:

package main

import (
	"fmt"
)

// Spoiler: WRONG CODE!
func Contains(type T)(haystack []T, needle T) bool {
	for _, current := range haystack {
		if current == needle {
			return true
		}
	}

	return false
}

func main() {
	numbers := []int{1, 2, 4, 8, 16, 32}

	fmt.Printf("Has 1? %t\n", Contains(numbers, 1))
	fmt.Printf("Has 5? %t\n", Contains(numbers, 5))
}

And… I failed miserably:

type checking failed for main prog.go2:9:6: cannot compare current == needle (operator == not defined for T)

I checked the updated design draft, and it can be easily solved using type contraints. After adding the constraint comparable to the type contract II used, it works as expected [play]:

package main

import (
	"fmt"
)

func Contains(type T comparable)(haystack []T, needle T) bool {
	for _, current := range haystack {
		if current == needle {
			return true
		}
	}

	return false
}

func main() {
	numbers := []int{1, 2, 4, 8, 16, 32}

	fmt.Printf("Has 1? %t\n", Contains(numbers, 1))
	fmt.Printf("Has 5? %t\n", Contains(numbers, 5))
}

And finally the output:

Has 1? true
Has 5? false

According to the Go blog,

if everybody is completely happy with the design draft and it does not require any further adjustments, the earliest that generics could be added to Go would be the Go 1.17 release, scheduled for August 2021. In reality, of course, there may be unforeseen problems, so this is an optimistic timeline; we can’t make any definite prediction.

I’ve been waiting for years, August 2021 is fine. I only hope the covid-19 vaccine arrives before. 🙂

More examples?

Adding more examples as I find them:

  1. Matt Layher wrote an implementation of a hashtable using generics. (June 17)