Skip to content

Instantly share code, notes, and snippets.

@arendtio
Created September 2, 2018 20:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save arendtio/77dd4df5f4b19dc69da350648434a88a to your computer and use it in GitHub Desktop.
Save arendtio/77dd4df5f4b19dc69da350648434a88a to your computer and use it in GitHub Desktop.
Generics in Go inspired by Interfaces

Introduction

For a while, I’ve seen the development around Generics in Go with some weird feeling. On the one hand, I was missing general container types, on the other side, I would not say I like the syntax that comes with generics and templates in other languages.

While writing Go code, I found Interfaces extremely useful, but they are still not nice-to-use for the use-cases you would like to use Generics for.

When the Go authors released their design drafts for the major features of Go 2.0, I was excited, but when I saw the draft for Generics, I was also concerned that they would kill the simplicity of the Go syntax. So I felt the need to explore my idea of a simple syntax for Generics in Go.

Origins

As I said I like Interfaces a lot and I love how they hide all the implementation details. When I write Go, the Interface is my contract with the world outside of my package, and as long as I don’t touch it, everybody is okay.

Currently, the recommended way of building container types in Go is to use Interfaces. If the container has no requirements towards the items it stores, we can use the empty Interface. However, the downside of this approach is, that when we receive elements from the container, the empty Interface guarantees for nothing.

So Interfaces have two functions:

  1. When they are used as types for input parameters, they require the type to have specific methods.
  2. When they are used as types for return parameters, they guarantee the type has specific methods.

While the first part would be just fine when using them as Generics, the second part is a problem. If you get back an element from a container with an empty Interface, you have no idea what you are getting back (type-system wise). So the only thing we can do is to use run-time type-asserts to find out if the element we got back is of the type we expect it to be.

Instead, it would be a lot nicer, if the compiler would check if the types we could get back, are okay for the things we want to use them for.

For example, let us use this simplified stack:

type simpleStack struct {
	stack []interface{}
}

func (s *simpleStack) Push(x interface{}) {
	s.stack = append(s.stack, x)
}

func (s *simpleStack) Pop() interface{} {
	l := len(s.stack)
	newest := s.stack[l-1]
	s.stack = s.stack[:l-1]
	return newest
}

Now let us build a function to store some ints and strings in it:

func prepareMixedStack() *simpleStack {
	s := &simpleStack{}
	r := rand.New(rand.NewSource(42))
	for i := 0; i < 10; i++ {
		if r.Intn(2) > 0 {
			s.Push(i)
		} else {
			s.Push(strconv.Itoa(i))
		}
	}
	return s
}

If we take an element from that stack now and fmt.Print it, everything should be fine, but if we would try to multiply two items with each other, we will run into problems sooner or later (https://play.golang.org/p/seUL1wXH5mg).

The Proposal

Changing the behavior of Interfaces would be a bad idea, as they are meant to hide things (e.g., implementation details). So instead I would like to add another type which is quite similar to Interfaces but comes with some automatic type checking to provide the compile-time type-safety we require for generics. In the wild it could look like this:

type myType generic{
	/*requirements*/
}

Syntax problem solved.

The tricky part is to let the compiler find out which types can come out of a container (the ones that get in somewhere) and what is expected from those types after they come out of the container.

To sum it up: while Interfaces guarantee the same things they require, the suggestion here is to implement Generics similar to Interfaces but with the difference of just letting them have requirements and determine their guarantees automatically by the compiler.

Beyond containers

With the draft design the Go authors also made clear that while containers are a popular use-case for Generics, there are other use-cases as well. Therefore, I took the examples from the overview document and modified them to reflect this proposal:

type K generic{}
type V generic{}
type T generic{}

// Keys returns the keys from a map.
func Keys(m map[K]V) []K

// Uniq filters repeated elements from a channel,
// returning a channel of the filtered data.
func Uniq(<-chan T) <-chan T

// Merge merges all data received on any of the channels,
// returning a channel of the merged data.
func Merge(chans ...<-chan T) <-chan T

// SortSlice sorts a slice of data using the given comparison function.
func SortSlice(data []T, less func(x, y T) bool)

The Empty Generic

In a world, without Generics the empty Interface was a nice idea to allow working with unknown types. With Generics there are hardly any reasonable use-cases for it. Instead the empty generic would take its place. Since the syntax for the empty Interface was annoying enough I suggest using the keyword ‘generic’ or ‘anything’ as placeholders for the empty generic{}.

Requirements Definition

You might have noticed that I didn't define if the 'requirements' should use the Interface syntax or the Contract syntax from the draft design. Actually, I would favor the Interface syntax (stricter, cleaner), but it would require more changes to the language (e.g., to support Operators), so the Contract syntax would work too.

Critical Reflection

One thing I dislike about this idea is the similarity of Interfaces and Generics as it might be hard for beginners to understand the difference (but maybe that is the price we have to pay).

I am no specialist in language design, and maybe I am missing an important aspect which prevents this idea, but I like it better to be the one with a bad idea who is getting told what he didn’t see, than to be the one who is dissatisfied with the result but didn’t participate in the discussion.

Conclusion

This proposal builds on one of the most powerful ideas within Go to bring Generics to the language to preserve the simple syntax we love. If it is possible to include such functionality within the Go compiler, I think it would be an improvement over the draft design.

@deanveloper
Copy link

Can you illustrate how you would make a Graph construct?

Under the proposal it looks like:

contract Graph(n Node, e Edge) {
	var edges []Edge = n.Edges()
	var nodes []Node = e.Nodes()
}

func ShortestPath(type N, E Graph)(src, dst N) []E { ... }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment