Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
deanveloper opened this issue Oct 17, 2018 · 7 comments

Comments

@deanveloper
Copy link

commented Oct 17, 2018

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.

@gopherbot gopherbot added this to the Proposal milestone Oct 17, 2018

@gopherbot gopherbot added the Proposal label Oct 17, 2018

@wsc1

This comment has been minimized.

Copy link

commented Oct 19, 2018

There is a workaround for this problem within the existing type system. To have a clonable interface C

type C interface {
   CloneC() C
   // other methods can be included as usual
}

you just make the method for cloning have a name specific to C.

Then, to implement with a concrete struct S, you can have

func (s *S) Clone() *S {
   // ... copy stuff and return it
}

func (s *S) CloneC() C {
   return s.Clone()
}

It is not as succinct as self but it avoids all the associated problems in the type system.

In my experience, real uses of self in a type system end up getting much more complicated than the simple examples used to motivate the usage.

@deanveloper

This comment has been minimized.

Copy link
Author

commented Oct 19, 2018

In my experience, real uses of self in a type system end up getting much more complicated than the simple examples used to motivate the usage.

It's noteworthy that many other type systems are much more complicated than Go's; when you have things like covariance then the self type can become complicated.

There is a workaround for this problem within the existing type system. To have a clonable interface C...

What you have provided isn't a terrible solution to the issue. It still suffers from designing your structs around the interface, though, rather than designing the interface around a pattern. For instance, I wouldn't be able to describe a recurring pattern in a foreign package.

@ianlancetaylor

This comment has been minimized.

Copy link
Contributor

commented Nov 27, 2018

I'm not clear on whether this can be implemented. Presumably Foo is assignable to Cloner if Foo has a method Clone() Foo. A value of type Cloner has a method Clone() Cloner; that is, when calling the Clone method of a Cloner, you get a Cloner back. So something somewhere must be converting the Foo returned by Foo's Clone method to a value of type Cloner. That can't be at the call site, since at the call site we don't know the dynamic type of the Cloner. So it must be at the point where Foo is converted to Cloner; we must write a wrapper function that we store in the Cloner method table. But then we have to ask what should happen if we first convert Foo to a interface{} type, and then (in some other package) we convert that interface{} to Cloner. That conversion must succeed, but we have no place to write the wrapper function. The package where the conversion occurs knows nothing about Foo.

It's also worth noting that self is an unusual name in that it can only appear as the result type of a method in an interface type. I don't think we can permit self as an argument type, and I don't think we can permit a result type of *self or []self or func(self) self or interface { M() self }.

@bcmills

This comment has been minimized.

Copy link
Member

commented Nov 28, 2018

That conversion must succeed, but we have no place to write the wrapper function. The package where the conversion occurs knows nothing about Foo.

I'm not sure what you mean here. The package where the conversion occurs has access to the type descriptor for Foo, and can at least figure out the concrete return type of its Clone method: presumably with that information it can construct a function that can wrap that type as a Cloner interface.

(I suspect we could even write such a function using the reflect package today, except that we don't have a way to instantiate the Cloner interface itself — that's #4146.)

@ianlancetaylor

This comment has been minimized.

Copy link
Contributor

commented Nov 28, 2018

@bcmills That is true, it may be possible to construct the function using reflect.MakeFunc. But that is a very high overhead approach for what appears to be a simple interface conversion. And the calls to the method will be surprisingly slow.

@mateuszmmeteo

This comment has been minimized.

Copy link

commented Jan 29, 2019

So basically you want to replace interface name in parameter type as "self" word.

Does the current solution not suit your needs? Why such complication?

This will affect changes in existing editors, libraries etc. Also "self" word is so "pythonish". Let's go more in C/C++ direction than Python.

@deanveloper

This comment has been minimized.

Copy link
Author

commented Jan 29, 2019

No, it's not the interface name

Look at the Cloner interface that I have provided, it matches func (T) Clone() T rather than func (T) Clone() Cloner. There is no way to represent this in current Go, and I have illustrated the problems with the generics "solution" to this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
6 participants
You can’t perform that action at this time.