Skip to content

proposal: Go 2: spec: add self type for use within interfaces #28254

Closed
@deanveloper

Description

@deanveloper

The Problem

Let's make an interface called Cloner, which describes things that can clone themselves.

As any good Go interface should be designed, it should describe behaviors rather than making implementations conform to the interface.

So, let's see our implementations:

type Foo struct { ... }
func (f *Foo) Clone() *Foo {
     newF := new(Foo)
     ...
     return newF
}

type Bar struct {...}
func (b *Bar) Clone() *Bar {
    // same as above, but with Bar
}

As you can see, the two Clone() methods have separate signatures. This is unfortunate as this means it is impossible to have an interface describe both types, even if they have similar patterns.

Current Solution

We could "solve" this issue like so:

type Cloner interface {
    Clone() Cloner
}

Then, we'd design the Foo and Bar functions to return Cloners rather than themselves.

This causes a few issues, though.

  • Since Cloners are returned, this means calling Clone() directly on a Foo or Bar would return a Cloner, which means we need to cast back to Foo, which is a big readability issue.
  • This is also less efficient when using Foo and Bar directly, as it causes unnecessary boxing/unboxing, although I'm not sure if the compiler optimizes this away.
  • This is also an anti-pattern, as we are designing our implementations around the interface, rather than making an interface that describes the implementations.

This is also apparently not the most intuitive solution, as it needs to be answered in the FAQ: https://golang.org/doc/faq#t_and_equal_interface

This is "solved" by generics, although not really. This is detailed at the bottom of the proposal.

The proposed solution: self

Instead, our Cloner interface would look as follows:

type Cloner interface {
    Clone() self
}

The implementations would not need to conform to the interface, so Foo.Clone would remain func (f *Foo) Clone() *Foo, and Bar.Clone would also remain the same.

Then, it could be used as follows:

func OurTestFunction(foo Foo, bar Bar, cloner Cloner) {
    fooClone := foo.Clone() // fooClone is type `Foo`
    barClone := bar.Clone() // barClone is type `Bar`
    someClone := cloner.Clone() // someClone is type `Cloner`
}

Bada-bing Bada-boom, we've solved our issue with relative simplicity.

Footnotes

Footnote: Syntax and compatability

"self" would also be a predeclared type, so if anyone has a type named "self" in their package, then that will be used rather than the predeclared self.

This goes without saying, but we aren't locked to the name "self" or anything. Also, adding a special character to show that the "self" type is special, such as @self or #self, may be a good idea. I am personally not a fan of this, as it don't feel very Go-like.

Footnote: Generics

I referenced above that the issue is partially solved by generics. This section illustrates how generics can solve some issues, but is not a full solution.

Here are the two solutions which can be used to solve our problem:

// Solution 1:
contract Cloneable(t T) {
    var t2 T = t.Clone()
}

// Solution 2 (if you really want an interface which can be evaluated at runtime):
type Cloner(type T) interface {
    Clone() T
}

Solution 1 prevents you from checking if t is Cloneable at runtime.
Solution 2 works, although it suffers from "Generic Poisoning" as then every function that wants to take a Cloner as an argument would then need parametric types.
Solution 2 ALSO suffers from the fact that T is not guaranteed to be the same as the receiver, leading to the following also implementing Cloner:

// A human
type Human struct { ... }
// A physical clone of a human. Human clones are not considered humans under our ethics board.
type HumanClone struct { ... }

func (h *Human) Clone() *HumanClone {
   // ...
}

This is not a true Cloner implementation! It returns a separate type from its receiver.

Metadata

Metadata

Assignees

No one assigned

    Labels

    FrozenDueToAgeLanguageChangeSuggested changes to the Go languageNeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.Proposalv2An incompatible library change

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions