Skip to content

Instantly share code, notes, and snippets.

@faiface
Created July 19, 2017 15:07
Show Gist options
  • Select an option

  • Save faiface/340cf2870ca8ac194b5d7269a89e5741 to your computer and use it in GitHub Desktop.

Select an option

Save faiface/340cf2870ca8ac194b5d7269a89e5741 to your computer and use it in GitHub Desktop.

Revisions

  1. faiface created this gist Jul 19, 2017.
    238 changes: 238 additions & 0 deletions .md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,238 @@
    # Ideas on generics in Go

    As we all know, Go 2 is on its way and despite the hesitations, it's clear that without generics, it
    would end up being a disappointment. And although we are still gathering real-life use-cases, or
    experience reports, there's nothing bad about starting to think about how generics in Go could look
    like. I've come up with a few ideas which I'd love to share. So, here we go!

    ## What's already achievable?

    Now, Go 1 has no generics, but has interfaces. Interfaces (sometime accompanied by reflection)
    actually let you write all kinds of generic code. Generic collections, generic functions, all are
    doable.

    Let's take a look, here's a very simple linked list:

    ```go
    type List struct {
    Value interface{}
    Next *List
    }
    ```

    This *is* a generic linked list. How do we use it?

    ```go
    var l *List
    l.PushFront(2)
    l.PushFront(3)
    l.PushFront(l.Value.(int) + l.Next.Value.(int))
    ```

    The trouble is all these type assertions. They come with these three downsides:

    1. More code.
    2. No compile errors, only runtime errors.
    3. Worse performance, because of boxing/unboxing.

    Let's take a look at another trivial example:

    ```go
    func Identity(x interface{}) interface{} {
    return x
    }

    func main() {
    x := 3
    y := Identity(x).(int)
    }
    ```

    We know that the return type is same as the argument type and we probably document it. *We just
    can't express it in the language*. And we want to express it in the language.

    ## The problem

    If we think about the examples above, what we wanted to express was that **some of those interface
    types are the same**.

    ```go
    func Identity(x interface{}) interface{} { // these two interface types are always same
    return x
    }
    ```

    ```go
    type List struct {
    Value interface{}
    Next *List // the interface value in Next is same as here
    }
    ```

    ## Idea number one - a bad idea

    Upon realizing what the problem is (ability to express the "sameness" of interfaces) I came up with
    this simple solution: bind the interface types by a name. Here's what I mean:

    ```go
    func Identity(x T interface{}) T interface{} {
    return x
    }
    ```

    As you can see, I added another identifier between `x` and `interface{}` and I added the same
    identifier before the return type. This indicates that the types are the same and thus it's possible
    to avoid type assertions.

    Now, why is this idea bad? It doesn't work with generic types.

    ```go
    type List struct {
    Value T interface{}
    Next *List
    }
    ```

    You see, there's nothing to bind. Even if we figured out a way to bind the `Value` type and the type
    inside `Next`, this would still be problematic:

    ```go
    func EmptyList() *List {
    return nil
    }
    ```

    The returned list is typeless, or is it? No one knows, it's ambiguous.

    ## Idea number two - probably a good one

    As we saw above, `List` type needs something like an "associated type" of its element. This type
    needs to be not only inferred by the compiler, it needs to be a part of the `List` type. Let's come
    up with a syntax:

    ```go
    type List[T interface{}] struct {
    Value T
    Next *List[T]
    }
    ```

    First of all, I hate angle brackets, so that's why I use square ones. Second thing to note is that
    `T` is followed by `interface{}`. What this means is that all things of type `T` are basically
    `interface{}`, *but* they have the same type. This also let's us express the identity function:

    ```go
    func Identity[T interface{}](x T) T {
    return x
    }
    ```

    Again, this let's us express that the argument and the return type are anything, but they are the
    same.

    ## Translating to Go 1

    A very important thing about this form of generics is that it's *fully translatable to Go 1*. Which
    means, that all Go 2 libraries utilizing generics would be perfectly usable from Go 1. Let's take a
    look:

    ```go
    // Go 2
    type Map[K, V interface{}] struct { /* ... */ }

    // Go 1
    type Map struct { /* ... */ }

    // Go 2
    func NewMap[K, V interface{}]() *Map[K, V]

    // Go 1
    func NewMap() *Map

    // Go 2
    func (m *Map) Get[K, V interface{}](key K) (val V)

    // Go 1
    func (m *Map) Get(key interface{}) (val interface{})

    // Go 2
    m := go2pkg.NewMap[string, int]()
    m.Set("hello", 2)
    x := m.Get("hello")

    // Go 1
    m := go2pkg.NewMap()
    m.Set("hello", 2)
    x := x.Get("hello").(int)

    // Go 2
    func Dictionary() *Map[string, string]

    // Go 1
    func Dictionary() *Map // but it's more than *Map[interface{}, interface{}], see below

    // Go 2
    dict := go2pkg.Dictionary()
    dict.Set("yay", 2) // compile error

    // Go 1
    dict := go2pkg.Dictionary() // type of dict is *go2pkg.Map
    dict.Set("yay", 2) // runtime error
    ```

    What you gain compared to Go 1 is *less typing*, *type safety* and *better performance* if the
    compiler takes care of true specializing (but it doesn't have to).

    ## Generic interfaces

    How about generic interfaces? For example:

    ```go
    type Producer[T interface{}] interface {
    Produce() T
    }
    ```

    The question is: what do I need to do to satisfy the `Producer` interface? And the answer is: that's
    not possible, you can't satisfy the `Producer` interface. You can only satisfy a
    `Producer[SomeType]` interface. And `Producer[int]` type is not assignable to
    `Producer[interface{}]` type. The reasons are the same as with "why isn't `[]float64` assignable to
    `[]interface{}`". Because the representations are different and taking generics this far would be
    too much.

    One problem arises with generic interfaces and that is: self-referential generic interfaces. A prime
    example of this is the `Equaler` interface. In Go 1, we can write:

    ```go
    type Equaler interface {
    Equal(Equaler) bool
    }
    ```

    The thing is, this is not type safe, because we can pass any equaler to the `Equal` function, but
    just the same type as the receiver. The Java way of solving this would be:

    ```go
    type Equaler[T Equaler[T]] interface {
    Equal(T) bool
    }
    ```

    However, I really don't like this. Supporting recursive generic type definitions is a too big hammer
    for something like this. It's quite unclear too. Ideally, something like this would be nice:

    ```go
    type Equaler[T interface{}] interface {
    (T) Equal(T) bool
    }
    ```

    This way, we bound the type of the receiver to the type of the argument. I'm not sure this is the
    good solution. Perhaps Go should not even support creating recursive generic interfaces at all.

    ## Thanks for reading

    I'm really glad you got this far. This ideas are not complete, that's why I'm not making it a Go 2
    proposal. This is to be improved, inspired from, or trashed.

    Michal Štrba