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

Closed
deanveloper opened this issue Oct 17, 2018 · 24 comments
Closed

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

deanveloper opened this issue Oct 17, 2018 · 24 comments
Labels
FrozenDueToAge LanguageChange NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. Proposal v2 A language change or incompatible library change
Milestone

Comments

@deanveloper
Copy link

deanveloper 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
@ianlancetaylor ianlancetaylor added LanguageChange v2 A language change or incompatible library change labels Oct 17, 2018
@wsc1
Copy link

wsc1 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
Copy link
Author

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
Copy link
Contributor

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
Copy link
Contributor

bcmills 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
Copy link
Contributor

@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.

@ianlancetaylor ianlancetaylor added the NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. label Dec 4, 2018
@mateuszmmeteo
Copy link

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
Copy link
Author

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.

@mier85
Copy link

mier85 commented May 22, 2021

Even though this issue is quite old, as I also often need this feature, and since we have the generic approach now, here are my two cents.

I played around with generics, but unfortunately they also don't seem to solve this particular problem. You can have generic Nodes and Edges, like described in the proposal, but they don't help you with the issue described here. If you want to return the interface itself, then you need to be aware of the interface.

Duck Typing would be much more complete when having something like the self keyword. This of course could also be combined with the generic approach. We already have the any keyword. We could have a self keyword that can only be used for generics and describe that T is referencing to itself:

type Cloner[C self] interface{
    Clone() C
}

Another example of where this would be helpful:

type LogFieldBuilder[C self, L Logger] interface {
  WithString(key, value string) C
  WithInt(key string, value int) C
  Build() L
}

This would enable us to finally have a type-safe structured-log that we can use when writing libraries.

@DeedleFake
Copy link

Can't LogFieldBuilder be done without referencing the underlying type?

type LogFieldBuilder[L Logger] interface {
  WithString(key, value string) LogFieldBuilder[L]
  WithInt(key string, value int) LogFieldBuilder[L]
  Build() L
}

Yeah, it'll return the interface, but the returned interface will still provide the same methods and the Build() method will return L regardless of how many chained With*() methods it's called after.

@mier85
Copy link

mier85 commented May 23, 2021

Can't LogFieldBuilder be done without referencing the underlying type?

type LogFieldBuilder[L Logger] interface {
  WithString(key, value string) LogFieldBuilder[L]
  WithInt(key string, value int) LogFieldBuilder[L]
  Build() L
}

Yeah, it'll return the interface, but the returned interface will still provide the same methods and the Build() method will return L regardless of how many chained With*() methods it's called after.

Unfortunately, when returning the interface, the implementation needs to be aware of the interface:

package log 

import "github.com/some/lib"

type Logger[L any] struct {
}

func (l *Logger[L]) WithString(key, value string) lib.LogFieldBuilder[L] {
   l.addString(key, value)
   return l
}

So now the logger would need to be aware of lib in order to implement the interface and these two would become tightly coupled. If the logger could just return itself, then the logger wouldn't need to be aware of the lib and the lib wouldn't need to be aware of the logger. Then whoever needs logging in the lib can just plug in any logger that implements the interface ( arguably a library shouldn't log much, but sometimes when handling internal processes, a debug log is still very helpful )

Additionally, when only having a self-argument in the generic argument list, it could be considered that we can always infer that, to prevent a bit of the "Generic Poisoning" ( mentioned by @deanveloper )

@christophberger
Copy link

A late update: The requested self-referential function signature inside the interface works well with type parameters in the current dev branch:

Playground

type Cloner[C any] interface {
	Clone() C
}

type Cloneable int

func (c Cloneable) Clone() Cloneable {
	return c
}

func CloneAll[T Cloner[T]](originals []T) (clones []T) {
	for _, c := range originals {
		clones = append(clones, c.Clone())
	}
	return clones
}

func main() {
	origs := []Cloneable{1, 2, 3, 4}
	fmt.Println(CloneAll(origs))
}

Note how the CloneAll function is parametrized with T Cloner[T]] to express that the parameter type T must be a type that implements the Cloner interface with itself (T) as the return type of Clone().

@mier85
Copy link

mier85 commented Dec 17, 2021

You are right @christophberger . It's not very easy to read but it is awesome that it works!

@christophberger
Copy link

christophberger commented Dec 19, 2021

@mier85 - I confess I have unconsciously increased the reading difficulty by choosing slice parameters (which means additional square brackets). I am working on a better example for a blog article, even though you beat me already at that! :)


Edit: for reference, here is my article: How to use generics for creating self-referring interfaces

@soypat
Copy link

soypat commented Dec 27, 2021

A personal experience that may shed some photons on the matter:

So I ran into this problem head-first during this weekend. I couldn't solve it without introducing some serious generic heavy-duty-machinery on the user's side since I had built a hierarchical generic interface (if that's what you call it?). In the end I ended up ditching the generic approach on the interface methods and generifying the algorithm's implementation. You can find a monologue at golangbridge.

@TAR5
Copy link

TAR5 commented Jan 23, 2022

I ran into this problem, when I tried to call an "overwritten" method, of a type with an embedded type.

package main

import "log"

type MyInterface interface {
	GetType() string
	SetSomething(s interface{}) MyInterface
}

type A struct{}

func (a *A) GetType() string {
	return "A!"
}

func (a *A) SetSomething(s interface{}) MyInterface {
	// sets something here...
	return a
}

// Type B has type A embedded:

type B struct {
	A
}

func (b *B) GetType() string {
	return "B!"
}

func main() {
	a := new(A)
	log.Printf("type: %s", a.GetType()) // Prints "A!"

	b := new(B)
	b.SetSomething("something")
	log.Printf("type: %s", b.GetType()) // Prints "B!"

	b2 := new(B).SetSomething("something")
	log.Printf("type: %s", b2.GetType()) // Prints "A!" because b2 is of type "MyInterface"
}

https://go.dev/play/p/2l_5OsLgzYM

@antichris
Copy link

@TAR5

when I tried to call an "overwritten" method

It's not what you did, not really. You called the GetType() of an A underlying a MyInterface that you confusingly named b2. There is no SetSomething() on B, and SetSomething() of A has a very explicit return a, which is what you got in b2.

The closest you can get to the fluent interface you seemed to be going for would be augmenting B with something like

func (b *B) SetSomething(s interface{}) MyInterface {
	b.A.SetSomething(s)
	return b
}

You have to keep in mind that Go does not have inheritance. The problems you are used to solving using inheritance in other languages you can't solve this way in Go.

@TAR5
Copy link

TAR5 commented Jan 29, 2022

@antichris

Thank you, now I understand that even if SetSomething had the return type self, a call to SetSomething on B only forwards it to the embedded type A, which would still return A.

@antichris
Copy link

antichris commented Jan 29, 2022

@TAR5, it's not even forwarding: when you call a method that is not defined on an embedding type, your call just falls through to the method on the embedded value, which is completely oblivious of anything but itself. But, yeah, you got the gist of it.

This proposal is about being able to satisfy an interface by returning the concrete (not interface) value. So that, for your example, you could have

func (a *A) SetSomething(v interface{}) A { /* ... */ }
func (b *B) SetSomething(v interface{}) B { /* ... */ }

satisfying

type MyInterface interface {
	SetSomething(v interface{}) <magic>
}

where <magic> is a keyword that allows the concrete type implementing MyInterface that the method is defined on.

@mgnsk
Copy link

mgnsk commented Jan 31, 2022

https://medium.com/@mier85/self-referencing-interfaces-in-golang-1-18-bcd6b5701992 Is this what you're looking for?

edit: didn't realize it's already poested here.

@Zyl9393
Copy link

Zyl9393 commented Mar 20, 2022

I believe I am having a comparable problem here where I need the interface to specify methods that can return values of the same type as they are called on. I don't see how it can be done given the current spec.

@ianlancetaylor
Copy link
Contributor

@Zyl9393 That is worth discussion, but not on this issue. As the Stack Overflow answer shows, there are ways to do what you want, even if they are less convenient than you desire.

I don't think there is anything to do on this issue now that we have generics and generic interface types. So I'm going to close this. Please comment if you disagree.

@soypat
Copy link

soypat commented May 9, 2022

Mail group discussion regarding gonum's mat.Matrix type. With the self proposal the mat package does not have to be imported into every module that wants to implement the Matrix interface. This saves modules implementing the common mat.Matrix interface a rather large dependency.

type Matrix[T self] interface {
        At(i, j int) float64
        Dims() (r, c int)
        T() T
}

@leaxoy
Copy link

leaxoy commented Apr 12, 2023

Sorry for bother everyone, I find another use case, when I try to import FromIter from rust to go.
I can't write the return type as the following without introduce self type:

type FromIter[T any] interface {
    FromIter(iter Iter[T]) ???
}

The return type are those who implement FromIter, not a specific type.

@christophberger
Copy link

Hi @leaxoy ,

A closed GitHub issue is perhaps not the best location to discuss a programming problem. I would suggest that you move your question over to a discussion group like the Go Forum or the Golang-Nuts mailing list.

You will get many more eyes on your issue in either of these places.

To move forward quickly, start there by providing the following information:

How is Iter defined?
What part of your code errors out?
What error do you get?

And maybe you can even provide an example of the exact problem in the Go Playground. I already tried to figure out what the problem is, but I seem not to have recreated your exact scenario.

See you in the Go Forum or in the Golang-Nuts mailing list!

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
FrozenDueToAge LanguageChange NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. Proposal v2 A language change or incompatible library change
Projects
None yet
Development

No branches or pull requests