I would like to propose the removal of embedded struct in Go 2.x, because it does not solve inheritance problems, and the alternatives may actually be better.
First, let us examine the pros and cons of inheritance (more specifically, implementation inheritance). Inheritance groups common functionalities into the base class. It allows us to make a change in one place (instead of being scattered everywhere), and it will be automatically propagated to the derived classes. However, it also means the derived classes will be highly coupled to the base classes. The base class implementor, who does not need to know anything about the derived classes, may inadvertently break the derived classes when making changes to the base class. In addition, inheritance encourages a type hierarchy. In order to understand a derived class, one need to know its base classes (which includes its immediate parents, its grandparents, its great grandparents, etc.). It gives us the classic OOP problem where in order to get a banana, you need the monkey and the whole jungle. Modern OOP proponents now encourage "composition over inheritance".
Go's embedded struct is unique. At a glance, it appears to be a composition. However, it behaves exactly like the classic inheritance, including the pros and cons of inheritance. The base struct implementor may inadvertently break the derived structs. In order to understand a derived struct, one must look up the definition of its base struct, creating a hierarchy of types. Thus, Go's embedded struct is actually inheritance (although it is a crippled one).
I say it is a crippled inheritance because Go's embedded struct does not provide the same flexibility as the actual inheritance. One such example is this:
driver.Drive(s60.Volvo.Car.Vehicle)
Even though s60 is a vehicle, in Go, you need to get to the actual base class in order to use it as an argument.
If the Go's Team considers keeping this inheritance property, I say they should either go all the way with inheritance, or scrap it altogether. My proposal is to scrap it.
Here are my reasons for scrapping it: 1. It offers no benefits. Although it looks like a composition, it is not. It behaves like inheritance with all the flaws of inheritance. It offers less flexibility than the real inheritance for no apparent benefits. 2. It does not take much effort to write forwarding methods. In fact, it may be better to do an actual composition without the embedded struct feature. The slight extra effort pays off in the long run. 3. If you truly do not want to write forwarding methods, you may refactor common methods into functions that accept a common interface. You can achieve the same effect as inheritance without having to couple the types. For instance, instead of
type BaseDriver struct{}
func (b BaseDriver) Drive(vehicle Vehicle){}
type TructDriver struct{
BaseDriver
}
type TaxiDriver struct{
BaseDriver
}
You can do this
func Drive(driver Driver, vehicle Vehicle) {}
Let me know what you think.
Thanks.
Henry
Comment From: griesemer
(I assume you mean removing embedded types in general, rather than just embedded structs, which are simply a common type to embed.)
Without casting any judgement on this proposal, I note that embedding is also a significant cause of complexity in the language and implementation.
Comment From: twitchyliquid64
I agree with your presentation of the limitations but disagree with your conclusion to remove it from the language. Embeddings are useful (and the limitations negligible) when you have lots of little types 'embedding' off another little one. It is far less cognitive load to think of subwoofer
as speaker plus wubdubdub
. I agree this is the wrong approach when you have really long recievers and complex composition though, then interfaces are a much better option.
Looking at your alternative, which you wrote as:
func Drive(driver Driver, vehicle Vehicle) {}
Imagine the realistic case where Driver
or Vehicle
have obscure interface methods, and there are a lot of implementations of Driver
and/or Vehicle
. Even having forwarder methods as you propose is a lot of duplication to deal with for function spec changes to the forwarded method (less of a problem nowadays with smart search-and-replace with IDEs).
Lastly, how often have we seen embedding used horrifically? I've seldom seen it (feel free to prove me wrong with links to stuff I'll need to follow up with eye-bleach), and I say if it aint broke dont fix it (especially considering it represents an additional break from Go1 compatibility).
Comment From: cznic
I would like to propose the removal of embedded struct in Go 2.x, because it does not solve inheritance problems, and the alternatives may actually be better.
Yes, it does not solve inheritance problems, because embedding is not intended to solve inheritance problems. Because it does not solve what it was not intended to solve, the above rationale about why to remove it makes no sense to me.
Comment From: as
I agree with neither the technical accuracy of the premise nor the final conclusion. Inheritance locks in functional dependencies in the base class. Embedding allows those dependencies to be overridden or propagated at a per-method granularity.
Comment From: valyala
I frequently use interface embedding when only a few interface methods must be overridden:
// StatsConn is a `net.Conn` that counts the number of bytes read
// from the underlying connection.
type StatsConn struct {
net.Conn
BytesRead uint64
}
func (sc *StatsConn) Read(p []byte) (int, error) {
n, err := sc.Conn.Read(p)
sc.BytesRead += uint64(n)
return n, err
}
or
// AuthListener is a `net.Listener` that rejects unauthorized connections
type AuthListener struct {
net.Listener
}
func (al *AuthListener) Accept() (net.Conn, error) {
for {
c, err := al.Listener.Accept()
if err != nil {
return c, err
}
if authorizeConn(c) {
return c, nil
}
c.Close()
}
}
There is no need in implementing other net.Conn
and net.Listener
methods thanks to method forwarding in these cases.
Comment From: henryas
@griesemer Yes. Embedded types is the more accurate term. I used embedded struct in the example because it is more evil compared to embedded interface. I would think that it shouldn't be that difficult to type out the full interface signature than relying on interface embedding.
@twitchyliquid64 Sorry if I didn't give a clear example earlier. What I meant to say was instead of doing the following:
type BaseDriver struct{}
func (b BaseDriver) Drive(vehicle Vehicle){}
type TruckDriver struct{
BaseDriver
}
type CabDriver struct{
BaseDriver
}
You have two alternatives. First, you can use the real composition and employ method forwarding, which would be as follows:
type TruckDriver struct{
parent BaseDriver
}
func (t TruckDriver) Drive(vehicle Vehicle){
t.parent.Drive(vehicle) //method forwarding
//...or you may implement custom functionalities.
}
It is true that it takes a little duplication (which requires little to no additional effort), but the given the benefit of having the TruckDriver decoupled from the BaseDriver and easier maintenance in the future, I say it's worth it.
Alternatively, you may refactor the common method into function. So the above code snippet will now look like this:
type Driver interface {} //which may be implemented by TruckDriver, CabDriver, etc.
func Drive(driver Driver, vehicle Vehicle){} //Drive is usable by any kind of driver.
The benefit of this approach is that each driver exists independently, and the Drive functionality is decoupled from any specific driver. You can change one or the other without worrying about unforeseen domino effect.
I prefer having my types flat rather than deep. The problem with type hierarchy is that it is difficult to tell what other things you may break when you make a slight alteration to your code.
Given that embedded types is a major feature in Go, I am actually surprised to see that it isn't used as much as I anticipate it to be. It may be due to its certain characteristics, or simply people having no particular need for it. At least, gophers don't go crazy with constructing the different families of apes (or may be not yet). Regardless, I don't agree with the "if it ain't broke, then don't fix it". I am more of a "broken windows theory" guy. Go 2.x is the opportunity to take lessons learned from Go 1.x and makes the necessary improvements. I would think that the resulting simplicity from the removal of unnecessary features may bring about other future opportunities (eg. other potentially good ideas that may be difficult to implement due to the complexity of the current implementation).
@cznic It behaves exactly like inheritance. Changes to the base type is automatically inherited by the derived types. If it does not solve the problems that inheritance is meant to solve, then I don't know what does.
@as I have no idea what you are talking about. Here is an illustration. The last I checked my Parents were normal human beings. Since I am meant to function as a normal human being, I inherit from them so that I too can be a human. However, one day, my Parents abruptly decide to grow scales and a pair of wings, and breath fire. Since I inherit from them, I am too automatically turned into a dragon (without my permission!). Since I am expected to function as a human, it breaks my intended functionality. I could insist in being human by overriding the inherited behavior from them. However, no one can tell what other crazy things they may do in the future that will again break my functionalities. That is doable in both inheritance and in Go's embedded types.
Comment From: cznic
It behaves exactly like inheritance. Changes to the base type is automatically inherited by the derived types.
It does not behaves like inheritance at all. Given
type T int
func (T) foo()
type U struct {
T
}
var u U
u.foo()
behaves exactly like u.T.foo()
when there's no foo
defined within U
- that's composition, not inheritance. It's only syntactic sugar with zero semantic change. When (T).foo
executes it doesn't have the slightest idea its receiver is emebded in some "parent" type. Nothing from T
gets inherited by U
. The syntactic sugar only saves typing the selector(s), nothing else is happenning.
Comment From: henryas
@cznic I agree with you that T is embedded in U and that is composition. However, the real issue is with the automatic propagation of T characteristics (even if those characteristics are still technically belong to T). Now, in order to learn what I can do with U (as a consumer of U), I have to learn what T does as well. If T changes, I as the consumer of U, have to be aware of those changes as well. This is the inheritance-like behavior I was talking about. It is simple if it involves only T and U. Imagine if T embeds X, and X embeds Y, and Y embeds Z. All of a sudden, you get the monkey and the banana problem.
Comment From: ianlancetaylor
In order for any proposal along these lines to be adopted, you'll need to do some analysis of where and how current Go programs use embedding, and how difficult it would be for them to adapt. You could start with just the standard library.
Comment From: as
@henryas
Since I am expected to function as a human, it breaks my intended functionality.
No, it enhances your functionality. If your intended behavior is to be Human, there is nothing the embedded type can do to change that or take it away. Sure, for some reason if the original type gained a method
BreatheFire(at complex128)
You could say you're say you're a dragon, but you're not. You're a fire breathing human who appreciates complex numbers. Your concerns seem to be about the general concept of functional dependency than about the inheritance problem. These exists in packages, interfaces, function parameter lists, and the list goes on.
Comment From: bcmills
The base struct implementor may inadvertently break the derived structs.
That claim is not obvious to me. Could you give some concrete examples?
Comment From: henryas
@ianlancetaylor I'll see what I can do.
@as It's actually pretty cool to be a fire-breathing human once in a while. But how much I will stay as a human depends on my earlier expectation of my Parents. If I thought that my Parents were already humans and therefore I didn't implement much humanity in me (because it was already implemented by my Parents), when my Parents turn into a giant lizard, I too will turn. Basically, I don't become what I expect myself to be, but I am actually the enhanced version of my Parents (whatever my Parents want to be). You may argue that this is technically the correct definition of inheritance (and it is), but it breaks the original assumption about me. The assumption about me depends on my assumption about my Parents, whereas my Parents don't necessarily know (and don't need to!) my assumption about them.
The main issue is that I don't encapsulate my Parents trait in me. When my client deals with me, they also have to deal with my Parents, and possibly my Grandparents as well. It creates unnecessary coupling and breaks encapsulation. Changes in one will have a domino effect on the others. With proper encapsulation, the effect of a change should be contained like ship compartmentation.
Again, it depends on whether this is the intended behavior that the Go's Team wants in Go language. If it is then I say they should not hesitate and support the proper inheritance all the way. After all, despite its criticism, inheritance is still a major feature in many popular programming languages today. If they do not want it, then they should scrap it and I vote for scrapping it.
Comment From: bcmills
It does not take much effort to write forwarding methods.
That is only true at small scales: at larger scales, the advantage of embedding is that you do not need to update forwarding for each newly-added method.
If you embed a BaseDriver
today and someone adds some new method to it (e.g. PayToll(amount Money) error
), all of the embedders will now implement the expanded method set too. On the other hand, with explicit forwarding methods you must locate and update all of the embedders before you can (say) add that method to a corresponding interface type.
Comment From: henryas
@bcmills The problem with automatic method implementation is that the people who write BaseDriver
may not know the context in which BaseDriver
are used by their derived drivers, and may inadvertently break the original assumption about the specific drivers. For instance, in your example, I may have certain drivers whom I may want them to pay toll using cash card that the company can control, rather than with cash. I may already have EnterTollRoad(card *CashCard)
method in my driver. The changes you make to the BaseDriver
just provided a way to circumvent the security measure I have in place for my specific driver.
Comment From: ianlancetaylor
@henryas I don't personally find that to be a particularly convincing argument. The fact that it is possible for a program to misuse some feature of the language does not imply that that language feature should be removed. You need to a longer argument; for example, you might argue that the misuse is a common error that people routinely make.
Comment From: henryas
@ianlancetaylor if you read my earlier posts, I did point out that ... well, I will summarize it again here: 1. The base type, that is supposed to not need to know anything about the derived types, has the ability to alter the derived types and break the original assumption about them. 2. The consumer of the derived types needs to know not only the derived types themselves, but also the base type. It introduces unnecessary coupling. The consumer of the derived types is now coupled to both derived types and the base types (and the base types' base types if the base types are also derived from other types). It makes the code fragile because a change to any of the base types will have a cascading effect to other parts of the code.
By the way, those are common arguments against inheritance and none of them is my invention. So I really don't need to prove their validity and how they apply in practice. They have been around for years, expounded by experts and practitioners alike. They are relevant to Go's embedded type because despite it not being technically an inheritance, it exhibits an inheritance-like behavior that causes it to share the same pros and cons of an inheritance.
As mentioned before, despite arguments against inheritance, inheritance is still widely used in many programming languages today. So it's really up to the Go's Team to decide which direction they want to take Go language to. In my opinion, embedded type is not a needed feature and can be removed from the language.
However, should the Go team decide to keep this feature, I would say there is no need to complicate it by making it look like an inheritance in disguise. After all, you are still getting all the side effects of a proper inheritance anyway. Just take the common road to inheritance, and boldly claim this is inheritance. It is simpler that way and you most likely have all the kinks already ironed out by other languages who share the same road.
Comment From: ianlancetaylor
@henryas Thanks, but I'm not asking for a list of possible errors. I'm asking for an argument that these are errors that people commonly make.
To put it another way, every reasonably powerful language has features that can be misused. But those features also have a benefit. The question, as with all language questions, is how to weigh the benefits against the drawbacks. The fact that a feature can be misused is a minor drawback. The fact that a feature frequently is misused is a major drawback. You've demonstrated a minor drawback. I'm asking whether you can demonstrate a major one.
We don't call embedded types inheritance because it is not inheritance as most people use the term in languages like C++ and Java. For us to call it inheritance would be confusing. See also https://golang.org/doc/faq#Is_Go_an_object-oriented_language and https://golang.org/doc/faq#inheritance .
Comment From: henryas
@ianlancetaylor What you are asking is difficult to quantify. How do you weigh one feature against another? How do you measure the benefits and the drawbacks, and whether they are major or minor? It isn't as simple as counting loc. The best that I can think of is by drawing opinions from everyone's experience with real-life projects whether they would rather have the feature or its absence.
In the Go projects that I have been involved with so far, there have been very little use of embedded types. If I recall correctly, we might have used embedded types for some small, simple types. We can live without the feature with no problem. It shouldn't take much effort to refactor the code. In fact, thinking back, it may be better to not use embedded types. However, considering the types involved are minor, small, and very unlikely to change, I would say, in our projects so far, it makes little difference between using the embedded types and not. Our team is also small. The longest distance between one person to another is just one table away. So the ease of communication may help in a way. In addition, the base and the derived types were usually written by the same person. So I would say it is quite difficult to judge the merits (or their absence) of embedded types from my own projects. However, I would think that embedded types is a superfluous feature.
About why I mention inheritance when the subject is about embedded types, I briefly mentioned this in the earlier post:
They are relevant to Go's embedded type because despite it not being technically an inheritance, it exhibits an inheritance-like behavior that causes it to share the same pros and cons of an inheritance.
I will elaborate further, and attempt to compare Go's embedded types to the common notions of inheritance and composition. I will use Parent and Child rather than Base and Derived.
Inheritance
What is inheritance? Inheritance is described as an "is-a" relationship between two types. The Parent defines the identity, and the Child is essentially the Parent with additional features. You can think of the Child as the enhanced version of the Parent. If the Parent is a fish, then the Child is a super fish. If the Parent is a bird, then the Child is a super bird.
How is this relationship implemented in practice? It is implemented by having the Parent's characteristics automatically exposed by the Child. Now let's compare this to Go's embedded types.
type Parent struct{}
func (p Parent) GrowFinsAndSwim(){} //Parent is a fish
//The child is automatically a fish
type Child struct{
Parent
}
func (c Child) TalkTo(person Person){}
//A super fish that can talk to human
child.GrowFinsAndSwim()
child.TalkTo(person)
Now, what if we changed the Parent to be a bird.
type Parent{}
func (p Parent) GrowWingsAndFly(){} //Parent is now a bird
//The child now becomes a bird
type Child struct{
Parent
}
func (c Child) TalkTo(person Person){}
//A super bird that can talk to human
child.GrowWingsAndFly()
child.TalkTo(person)
As seen from the above illustration, Go's embedded types behaves like inheritance. The Parent defines the identity, and the Child is the Parent with some enhancements.
Another aspect of inheritance is the Child's ability to override the Parent's behavior to provide more specialized behavior. We'll see how we can do that with Go's embedded type.
type Parent{}
func (p Parent) GrowWingsAndFly(){} //Parent is a bird
//The super bird
type Child struct{
Parent
gps GPS
}
func (c Child) GrowWingsAndFly(){}
//Unlike the Parent, this super bird uses GPS to fly.
child.GrowWingsAndFly()
Again, we see inheritance-like behavior in Go's embedded type.
Now, how do you implement inheritance? Different languages may have different implementations, but I would think that the Child would need to hold some kind of reference to the Parent in order for it to work. If you describe it in Go, it would look something like this:
type Child struct{
*Parent
}
Voila! It is embedded type! Is Go's embedded type actually an inheritance? You decide.
However, there is an important difference between Go's embedded type and the normal inheritance. The normal inheritance exposes only the Parent's behavior. On the other hand, Go's exposes both the Parent's behavior and the Parent itself. The question to ask is whether there is any benefit to doing so?
Composition
Now, we will compare Go's embedded type to composition. Composition is defined as a "has-a" relationship between two types. A Person has a Pencil, but the Person is definitely not a Pencil. The Child controls its own identity and the Parent has no control whatsoever over the Child. In Go, proper composition should look like this:
type Person struct{
pencil Pencil
}
func (p Person) Write (book Book) {
//do something with pencil and book
}
The important thing to note here is that Pencil is well encapsulated by Person. The definition of Pencil does not automatically apply to Person. If you try to use embedded type to describe this "has a" relationship:
type Person struct{
Pencil
}
type Pencil struct{}
func (p Pencil) Brand() string{}
func (p Pencil) Manufacturer() string{}
//Okay
person.Pencil.Brand()
//But ... Ouch!
person.Manufacturer()
person.Brand()
that very definition of "has a" (composition) will break, as embedded type resembles more of an "is a" (inheritance) relationship than a "has a" relationship.
Conclusion
Let's start by calling a spade a spade. Go's embedded type is inheritance. It describes an "is a" rather than a "has a" relationship. Technically, it is inheritance disguised as composition. The thing is why do it in a queer roundabout way if what you need is plain inheritance. Is there any benefit to doing so? I think Go 2.x is a great opportunity to fix it or ditch it.
Comment From: davecheney
Let's start by calling a spade a spade. Go's embedded type is inheritance
It is not.
The thing is why do it in a roundabout way if what you need is plain inheritance.
Go does not have inheritance, so embedding is not a round about way of providing inheritance as Go does not provide that feature. In Go value types satisfy the "is-a" relationship, and interface types satisfy the "has-a" relationship.
Comment From: cznic
@davecheney Is the last sentence saying what you want to wrote? It seems to me it's the other way around. Or I'm really confused before the first coffee of the day.
Comment From: henryas
@davecheney I am puzzled.
In Go value types satisfy the "is-a" relationship
If Go value types satisfy the "is-a" relationship, how do you describe "A Carpenter is a Person that build furniture. A Singer is a Person that sings" in Go value types?
and interface types satisfy the "has-a" relationship.
type Pencil struct{}
type Person struct{
pencil Pencil
}
func (p *Person) SetPencil(pencil Pencil){}
func (p *Person) Pencil() Pencil{}
````
The above code has no interface whatsoever, and yet the Person _"has a"_ Pencil.
Where does that leave embedded types? What is the relationship between the embedded and the embeddee?
**Comment From: davecheney**
@cznic yup, that's about as clear as I can make it
@henryas
> A Carpenter is a Person that build furniture. A Singer is a Person that sings" in Go value types?
A `type Carpenters struct{ ... }` _is-a_ `Carpenter` nothing more, nothing less. A Carpenter cannot be assigned to any other named type in Go.
A `type Person struct{ ... }` fulfils the `type Singer interface{ ... }` because it _has-a_ `func (p *Person) Sing() { ... }` method.
**Comment From: davecheney**
@henryas
> Where does that leave embedded types? What is the relationship between the embedded and the embeddee?
There is no relationship. `type Pencil struct { ... }` has no knowledge that it has been embedded inside another type, and that embedding has no effect on its behaviour.
**Comment From: davecheney**
@henryas
> The above code has no interface whatsoever, and yet the Person "has a" Pencil.
This is not correct, `type Person struct { ... }` has a method called `Pencil()` and a field called `pencil`. As `pencil` is not embedded into `Person`, this is neither inheritance, nor embedding.
**Comment From: as**
>In Go, proper composition should look like this:
type Person struct{ pencil Pencil } func (p Person) Write (book Book) { //do something with pencil and book }
Go supports this already, at the risk of sounding sarcastic, these are just `struct fields`. The value of `anonymous structs` is that they propagate structure and function at the `field` level whereas inheritance propagates function at the `type` level. Inheritance is a crippled construct such that the structure can not be altered down the creek and the user has to deal with the contract defined in the _base class_.
**Comment From: henryas**
** @davecheney ** I am still puzzled. A Carpenter is a specialized version of A Person. A Singer is another specialized version of a Person. Each has different skills. How do you present this "is a" relationship in value type? How do you associate Carpenter to Person, and Singer to Person?
Also, the code snippet I showed wasn't about embedded type. Sorry if I wasn't clear. It was to show the "has a" relationship without resorting to interface. The question was how interface comes to play into this and whether interface is the defining feature to present a "has a" relationship?
About embedded types, it is true that the embedded does not know about the embeddee. However, the embedee is aware about the embedded. At first, it looks like a composition. However, there is a syntactic sugar feature that allows you to access embedded's behavior without being explicit that you are accessing the embedded type. In the example I gave above, `person.Pencil.Manufacturer()` is clearer than `person.Manufacturer()`, which is misleading.
** @as** Yes, Go already supports that. That is what the normal composition looks like. That's how it is done in most other languages. The proposal is not to introduce new features. The proposal is to scrap embedded types, because embedded type is not needed for composition. If embedded type is needed for inheritance, then it is something else entirely. The problem is people keep saying embedded type is not inheritance. Some say it is composition, but we can already do composition without embedded type. Now, **davecheney** says embedded type is neither a "has a" nor a "is a" relationship, and that there is no relationship between the embedded and the embeddee.
I am puzzled. Maybe someone can better explain to why we need embedded types?
**Comment From: davecheney**
@henryas
> ** @davecheney ** I am still puzzled. A Carpenter is a specialized version of A Person. A Singer is another specialized version of a Person. Each has different skills. How do you present this "is a" relationship in value type? How do you associate Carpenter to Person, and Singer to Person?
A `Carpenter` is _not_ a specialised version of a `Person` in Go because you cannot write that code. We can only talk about what is possible to write in Go, not the metaphysical.
> I am puzzled. Maybe someone can better explain to why we need embedded types?
To make it pleasant to compose types that implement interfaces.
**Comment From: cznic**
@davecheney
> @cznic yup, that's about as clear as I can make it
Well, then let's look at it again. You wrote
> In Go value types satisfy the "is-a" relationship, and interface types satisfy the "has-a" relationship.
Wikipedia thinks ["is-a"](https://en.wikipedia.org/wiki/Is-a) is about inheritance/subtyping and that ["has-a"](https://en.wikipedia.org/wiki/Has-a) is composition. Plugging into the above I get something like
> In Go value types support subtyping/inheritance, and interface types support composition.
**Comment From: davecheney**
@cznic
> Wikipedia thinks "is-a" is about inheritance/subtyping and that "has-a" is composition. Plugging into the above I get something like
That's why I wrote
> A type Carpenters struct{ ... } is-a Carpenter nothing more, nothing less. A Carpenter cannot be assigned to any other named type in Go.
That is, there is no inheritance in Go. A `Carpenter` is _only_ a `Carpenter`.
**Comment From: as**
>I am puzzled. Maybe someone can better explain to why we need embedded types?
It seems that theoretical discussions will not advance the topic. I would like to know how you would construct AddressBar bar without embedding *Frame. Assume all 31 methods of Frame are needed.
type AddressBar struct{ url string *Frame } func (s AddressBar) String() string{ return a.url}
// Frame API - This exists in another package somewhere
func New(r image.Rectangle, ft font.Font, b image.RGBA, cols Color, runes ...bool) *Frame
func (f Frame) Bounds() image.Rectangle func (f Frame) Close() error func (f Frame) Delete(p0, p1 int64) int func (f Frame) Dirty() bool func (f Frame) Dot() (p0, p1 int64) func (f Frame) Full() bool func (f Frame) Grid(pt image.Point) image.Point func (f Frame) IndexOf(pt image.Point) int64 func (f Frame) Insert(s []byte, p0 int64) (wrote int) func (f Frame) Len() int64 func (f Frame) Line() int func (f Frame) Mark() func (f Frame) MaxLine() int func (f Frame) Paint(p0, p1 image.Point, col image.Image) func (f Frame) PointOf(p int64) image.Point func (f Frame) RGBA() image.RGBA func (f Frame) Recolor(pt image.Point, p0, p1 int64, cols Palette) func (f Frame) Redraw(pt image.Point, p0, p1 int64, issel bool) func (f Frame) RedrawAt(pt image.Point, text, back image.Image) func (f Frame) Refresh() func (f Frame) Reset(r image.Rectangle, b image.RGBA, ft font.Font) func (f Frame) Select(p0, p1 int64) func (f Frame) SetDirty(dirty bool) func (f Frame) SetFont(ft font.Font) func (f Frame) SetOp(op draw.Op) func (f Frame) SetTick(style int) func (f Frame) Size() image.Point func (f Frame) Sweep(ep EventPipe, flush func()) func (f Frame) Tick() func (f Frame) Untick()
**Comment From: ianlancetaylor**
@henryas Yes, it is difficult to weigh one feature against another, but when considering changes to the language that is what we must do.
Right now the language has the feature of embedded types with method promotion. Removing that feature would break, at the very least, thousands of programs. We aren't going to break thousands of programs on a whim. We need a good reason.
An example of a good reason would be a set of user experience reports (https://github.com/golang/go/wiki/ExperienceReports) from different people writing substantial Go code who ran into subtle bugs because of their use of embedded types. Another example would be an analysis of Github repos that use Go code in which people changed their program to stop embedded types, with some examination of why they made that change. Another example would be a large number of people supporting this proposal even knowing that it would break code (as I write this the reaction on the initial proposal is three thumbs up, fifteen thumbs down).
We are not going to remove a significant, widely used, feature that has been in the language since the public release without a very good reason. Intuition is not enough.
**Comment From: jimmyfrasche**
To be fair, code generation could take care of *most* of the features provided by type embedding.
Creating methods to forward calls to methods with the same name and signature is trivial (maintaining them as things change, less so, but doable with adequate tooling).
If that were the only thing type embedding provided, there could be an argument for removing the feature and updating code that used it would be trivial.
However, these can *only* be done with type embedding now:
- promoting fields of a struct embedded in a struct
- promoting unexported methods from a type in a different package
Even if there were agreement that type embedding should be removed, updating any code that used those two features (and there is a lot of it) would be easy to detect statically but very difficult to actually replace, if possible at all.
Promoted fields would at worst be time consuming, though perhaps possible to handle by a go fix style update.
Promoted unexported methods could require re-architecting entire systems since the interfaces satisfied by the types would change in a way that would be impossible to replace.
**Comment From: henryas**
**@as** It depends on how you would conceptually relate AddressBar to Frame. If AddressBar is a specialized version of Frame, then using embedded type is appropriate. It is an "is-a" relationship and the classic case of an inheritance. The Go code would be as follows:
````
type AddressBar struct {
*Frame
}
````
Actually, there is nothing wrong with inheritance. Like all features, it has its pros and cons. Major programming languages today still use it. I am just baffled by the fact that some people are in denial of the fact that you have inheritance in Go.
However, in the spirit of this proposal, I am going to show that there are other interesting ways to solve the same problem without resorting to using embedded types.
First, notice that Frame handles the visual element, while AddressBar is just some data to display. By using composition, we can do this:
````
type Frame struct {
content FrameContent
}
//followed by other definition of Frame..
type FrameContent interface {} //FrameContent defines the signature of AddressBar, SearchBar, etc.
//Now we can do this
frame1:= NewFrame(addressBar)
frame1.Show()
frame2:=NewFrame(searchBar)
frame2.Show()
frame3:=NewFrame(NewCompositeFrame(addressBar, searchBar))
frame3.Show()
````
Now, let's say Frame is already written beforehand by someone else, and it is frozen. There is no way you can make modification to it to accommodate your code. It requires security clearance level 5 and you are just the new guy with access to only your cubicle and the restroom. You need Frame and Frame is beyond your control. The simplest way is to just wrap Frame and use forwarding methods.
````
type AddressBar struct{
frame *Frame
}
func (a *AddressBar) Bound() image.Rectangle {
return a.frame.Bound()
}
func (a *AddressBar) Close() error {
//you may perform additional clean up for AddressBar here ..
return a.frame.Close()
}
func (a *AddressBar) Delete(p0, p1 int64) int {
return a.frame.Delete(p0, p1)
}
//... and the rest of the Frame's methods here.
//Note that you do not need to implement all Frame's 31 methods.
//Just pick the ones that are relevant to AddressBar,
//but you may implement all of them as long as they are relevant.
````
At first, it seems it takes some extra work to write all those forwarding methods, but it is easy and it really doesn't require that much effort (even if you have 31 methods to write). The process is mostly mechanical with barely any thinking involved. It doesn't even require much time. However, the little effort spent here is a well-spent investment. It limits the cascading effect of any change to Frame. Remember that the guy with the level 5 clearance may be a mean guy and may not even know you. If you have extremely high test coverage to lock down your code, you may be able to detect the impacts of the changes that the level 5 guy made to Frame, and fix any bugs it causes to your work without resorting to such method. However, in reality, it is often not practical to have 100% test coverage. In my own typical projects, we usually get about 55-65% test coverage. That means we have about 35-45% of _movable_ code that may deviate from the specs without alerting us. While it won't prevent all the bugs caused by Frame, it helps localize any potential bug.
Alternatively, you can resort to procedural styles. Sometimes things that are complicated to express in terms of objects may be simpler to express in procedures.
````
package frame
type Framer interface{}
type FramerWithBorder interface{} //Frame with specialized behavior (eg. AddressBar, SearchBar)
func Bounds(f Framer) image.Rectangle
func Close(f Framer) error
func Delete(f Framer, p0, p1 int64) int
func Dirty(f Framer) bool
func Dot(f Framer) (p0, p1 int64)
func Full(f Framer) bool
func Grid(f Framer, pt image.Point) image.Point
func IndexOf(f Framer, pt image.Point) int64
func Insert(f Framer, s []byte, p0 int64) (wrote int)
func Len(f Framer) int64
func Line(f Framer) int
func Mark(f Framer)
func MaxLine(f Framer) int
func Paint(f Framer, p0, p1 image.Point, col image.Image)
func PointOf(f Framer, p int64) image.Point
func RGBA(f Framer) *image.RGBA
func Recolor(f Framer, pt image.Point, p0, p1 int64, cols Palette)
func Redraw(f Framer, pt image.Point, p0, p1 int64, issel bool)
func RedrawAt(f Framer, pt image.Point, text, back image.Image)
func Refresh(f Framer)
func Reset(f Framer, r image.Rectangle, b *image.RGBA, ft *font.Font)
func Select(f Framer, p0, p1 int64)
func SetDirty(f Framer, dirty bool)
func SetFont(f Framer, ft *font.Font)
func SetOp(f Framer, op draw.Op)
func SetTick(f Framer, style int)
func Size(f Framer) image.Point
func Sweep(f Framer, ep EventPipe, flush func())
func Tick(f Framer)
func Untick(f Framer)
func ShowBorder(f FramerWithBorder)
func HideBorder(f FramerWithBorder)
````
The benefit of this approach is that each type and each function exist independently. The change in one does not impact the others. The given example may not be the best example for this scenario, but I have seen this pattern done to solve a fairly complex OO structure into an elegant solution. It brings down the complex type hierarchy into flat independent components that can be plugged into one another.
However, when you have 31 methods, it may be time to examine whether you really need all of them. LOL.
**@ianlancetaylor** If you are concerned about breaking changes, my suggestion is to just ignore this proposal and leave embedded type as it is. As mentioned, the proposal is about embedded type being an extraneous feature. There is really no harm in leaving it as it is. If people don't like it, they can just not use it.
However, if the Go Team is looking for potential candidates to trim down the language, embedded type should be considered. That's my proposal.
**@davecheney** I still don't understand what you are talking about. In my limited experience, when you start hearing people speaking like Master Yoda, it's time to call it a day and wait till you sober up. Perhaps I should re-read your posts again in the morning.
**Comment From: as**
>I am just baffled by the fact that some people are in denial of the fact that you have inheritance in Go.
Formal definitions are important because they allow opinions to exist separately from fact. Disagreeing with the common knowledge that `Go does not support inheritance` and even being baffled by it is well within your right, but it only sets your proposal back.
>At first, it seems it takes some extra work to write all those forwarding methods, but it is easy and it really doesn't require that much effort (even if you have 31 methods to write). The process is mostly mechanical with barely any thinking involved. It doesn't even require much time.
I notice only 3-4 were explicitly written out, and only the signatures. My example effectively summarized all 31 methods. This seems to contradict some assertions that embedding is confusing and suggests that embedding is a concise and elegant way of expressing structure even outside of the actual codebase.
> However, when you have 31 methods, it may be time to examine whether you really need all of them. LOL.
31 is a large number for playground examples, not so much for production code. Laughing out loud doesn't help the number go down either. The requirement is that you need the 31 methods, writing a comment saying you don't need them isn't a valid solution, and would likely lead to undesirable outcomes.
I will say that in the 6 years I've used Go, not once have I ever seen a bug that resulted from an embedded type. Do you have any concrete examples of the contrary?
**Comment From: bcmills**
> Do you have any concrete examples of [bugs due to embedded types]?
I don't support this proposal, but In the interest of fairness I do have such an example. The interface `proto.Message` is a tag-interface that represents “types generated by the protobuf compiler”. If a type that implements `proto.Message` is embedded in a struct, that struct will appear to also implement `proto.Message`, but may cause a panic if passed to a function that expects only generated message types.
(That's https://github.com/golang/protobuf/issues/364, and it's arguably more a misuse of interfaces than a defect of embedding proper.)
**Comment From: bcmills**
@davecheney, you are using _is-a_ and _has-a_ in the opposite of their usual senses in a programming context. You may be doing so intentionally to make a point, but it comes across as either confused or confusing.
Go interfaces have the usual OOP _is-a_ relationship if you map the OOP _is-a_ concept to its closest Go analogue, _is-assignable-to_. In that sense, a `net.Error` _is-a_ `error`, because the method set of `net.Error` is a superset of the method set of `error`.
In that sense, interfaces embedded into structs _do_ somewhat blend _is-a_ and _has-a_: a struct that embeds a `sync.Locker`, for example, both _is-a_ `sync.Locker` (because it has the methods required of a `sync.Locker`) and _has-a_ `sync.Locker` (addressable via the `Locker` field).
**Comment From: bcmills**
@henryas When we say that “Go does not have inheritance”, what we generally mean is that Go does not have dynamic dispatch: you can forward the methods of a type by embedding, but you cannot “override” its methods. Within the methods of the embedded type, all (recursive) method calls will go to the definitions in the embedded type, not the redefinitions of those methods in the embedding type.
**Comment From: bcmills**
https://github.com/golang/go/issues/18617 describes another problem with embedding-as-composition (with https://github.com/golang/protobuf/issues/276 as its concrete example). Namely, embedding has a surprising (and often frustrating) interaction with `nil` receivers. However, I believe it would be easier to remedy that problem by redefining the behavior of embedded methods with `nil` receivers, rather than by eliminating embedding entirely.
**Comment From: bcmills**
@henryas
> The problem with automatic method implementation is that the people […] may inadvertently break the original assumption about the [embedding types].
If you embed one Go type in another, you must not assume the non-existence of methods on the embedded type. This expectation is clearly documented in the [Go 1 compatibility policy](https://golang.org/doc/go1compat#expectations).
In other words: if you break a user's assumptions by adding a method, the user's assumptions were wrong. The two `proto` examples I mentioned above notwithstanding (as both are due to abuse of interfaces in the `proto` package), I am not aware of widespread patterns of errors of that sort.
**Comment From: as**
@bcmills
I am not in favor of this proposal either, but ever since mentioning that I've not seen a problem with embedding-as-composition I have been finding issues about it. Probably due to an increased awareness that they exist.
https://github.com/golang/go/issues/22172
**Comment From: bcmills**
Nice example. The same issue comes up fairly often with `fmt.Stringer` and other magic-method interfaces (see also #4146).
**Comment From: nigeltao**
> Do you have any concrete examples of [bugs due to embedded types]?
Here's a bug from Go's flavor of the App Engine Datastore. I can't remember the exact details, but it's along these lines...
---
Briefly, when serializing, Go structs' field names are flattened so that if you have:
type Foo struct { B Bar }
type Bar struct { X int }
and you serialize a Foo, you'll get a "B.X" field.
---
You can embed the Bar:
type Foo struct { Bar }
type Bar struct { X int }
Importantly, the flattened field name is "X", not "Bar.X". Doing it this way affects the queries you'd write (in GQL) and seemed consistent with how you'd write actual, idiomatic Go code: "foo.X = etc".
In hindsight, we might have done it differently, but we didn't have hindsight then, and once you store data in production, you can't change the underlying (language-agnostic) database's field names lightly.
---
Separately, the Go App Engine Datastore also lets you store a time.Time value. This has special semantics, so you can query the datastore by time (the concept), not just time-as-a-structure-with-integers. If you had
type Qux struct { T time.Time }
Then it's serialized as a single field (called "T"), not multiple fields ("T.wall", "T.ext", etc), one for each member of the time.Time struct. In hindsight, this assumes that that Go field _has a name_ (e.g. "T").
---
We had a bug because these two features mix: it's not recommended (and the interaction wasn't forseen at design time), but you can indeed embed a time.Time inside the top-level Foo struct that you're serializing:
type Foo struct { time.Time }
The question is how to flatten such a Foo to a wire format that the server accepts. Remember that for the earlier example, the flattened field name is "X", not "Bar.X". Thus, the field names here should be something like "X" and not "Time.X".
On the other hand, a time.Time struct is a struct that's treated specially, so we don't look at the "X" fields of the struct. Besides, such fields are unexported (and change over Go revisions, with the addition of monotonic time). So there is no "X".
There's no "Time" and no "X", so the flattened field name is the empty string "". The server didn't like that.
---
This bug was due to an interaction with embedding and something else, not just with embedding per se, but to me, it reinforces @griesemer's earlier quote:
> Without casting any judgement on this proposal, I note that embedding is also a significant cause of complexity in the language and implementation.
Not just for language and implementation, but I suspect it also causes complexity for serialization libraries such as JSON and XML.
**Comment From: nigeltao**
As for casting judgement, I've been bitten enough times (once is enough!) that I'd consider removing embedding, if we were starting again from scratch. As for Go 2.0, I haven't been following such discussions closely, but there might be a bigger concern of incrementally migrating Go 1 to Go 2, that trumps being able to simply remove embedding. I don't know.
OTOH, I do define a number of FooEmbed types in https://godoc.org/golang.org/x/exp/shiny/widget/node and as their name suggests, they are designed specifically for embedding into other structs. Values of such structs form a tree of GUI widget nodes. Those nodes don't all have to have the same concrete type, but I still wanted to share some common fields, such as the parent/child links.
These FooEmbed types are just a convenience, of course, as it would be straightforward for the embedders to explicitly rather than implicitly forward those methods on. Straightforward and mechanical, but certainly less convenient. Less magical too, in both the good and bad senses.
**Comment From: bcmills**
> These FooEmbed types are just a convenience, of course, as it would be straightforward for the embedders to explicitly rather than implicitly forward those methods on.
That isn't true of every embedded type, though. For example, the strategy that I proposed in https://github.com/golang/protobuf/issues/276 for removing the (awkward and distracting) `XXX_` fields from generated Protocol Buffer types makes use of unexported methods on the embedded type, which embedders cannot forward explicitly.
**Comment From: nigeltao**
> That isn't true of every embedded type
Ah, I forgot about that. I suppose that that just reinforces @griesemer's point that embedding leads to complexity.
**Comment From: owais**
It seems the method promotion is being confused for inheritance just because we can dispatch methods using the instance of type that embeds other types. My understanding of the whole concept might be wrong but I think the following example clarifies the difference.
```golang
package main
import (
"fmt"
)
type Y struct {
Value string
}
func (y Y) GetY() string {
return y.Value
}
type X struct {
*Y
Value string
}
func (x X) GetX() string {
return x.Value
}
func main() {
x := X{Y: &Y{Value: "Y"}, Value: "X"}
fmt.Println(x.GetX)
fmt.Println(x.Value)
fmt.Println(x.GetY())
fmt.Println(x.Y.Value)
}
would result in
X
X
Y
Y
Here X embeds Y but X.y is still an instance of type Y. X is not adopting Y's behavior and creating 1 final instance that represents both X and Y. X only creates an instance of itself while holding onto a reference to another instance of Y.
Consider these 2 rough python examples:
Inheritance:
class X:
value = 1
class Y(X):
value = 2
vs composition
class X:
value = 1
class Y;
x = X()
value = 2
In the second python example, we would have to define pass-through methods on Y if we wanted to call methods on Y.x directly from an instance of Y. If python did this automatically for us, would we consider it inheritance? It would still be composition but with some convenience. That is exactly what Go does.
So I guess the question is whether method promotion is useful enough or not in Go. I think it is quite useful and in no way adds inheritance to Go.
Comment From: davecheney
X is still an instance of type Y
This is not correct. An X cannot be substituted for a Y in Go, with the exception of if Y is an interface and X implements Y’s method set.
On 11 Nov 2017, at 15:32, Owais Lone notifications@github.com wrote:
X is still an instance of type Y
Comment From: davecheney
Ignore me, I quoted too narrowly and missed the context of your point. My apologies.
On 11 Nov 2017, at 15:37, Dave Cheney dave@cheney.net wrote:
X is still an instance of type Y
This is not correct. An X cannot be substituted for a Y in Go, with the exception of if Y is an interface and X implements Y’s method set.
On 11 Nov 2017, at 15:32, Owais Lone notifications@github.com wrote:
X is still an instance of type Y
Comment From: owais
Not your fault. It is a badly written sentence :)
Comment From: metakeule
@henryas
To avoid the "base class extension breaks the derived class" issue: Maybe it would be enough to forbid embedding of types that are defined inside other packages? Combine this with a godoc change to show methods of embedded types as special marked methods of the derived type for visibility.
Comment From: henryas
Well, first of all, the original intention of this proposal is to re-examine Go's embedded type feature and whether it is superior to existing approaches and alternatives used by other languages, and ultimately to determine whether the feature is necessary. The specific behavior being questioned is the automatic method promotion.
What is the relationship between the embedded and the embeddee that it warrants the automatic method fallover? Wikipedia states there are two types of relationships, the "is-a" and the "has-a". The automatic method promotion seems to satisfy the "is-a" relationship, but some people claim that it isn't an "is-a". However, at the same time you don't need method promotion for the "has-a". While "Man.Dog.WiggleTail()" makes sense, with automatic method fallover, "Man.WiggleTail()" suddenly becomes very wrong. While "Mike.Speak()" is okay, "Mike.Human.Speak()" is somewhat weird. Perhaps Go's embedded type behavior is satisfying another type of relationship that we are not familiar with?
Is Go's embedded type feature superior to existing approaches (eg. inheritance)? There is actually nothing wrong with inheritance. It is a feature used by many modern programming languages today. Like all features, inheritance has its uses and can also be abused. It allows the centralization of common behavior, while at the same time it couples the derived to the base class. Does the embedded type feature adds values over the conventional inheritance approach? Why not use the conventional inheritance? It is a path well-trodden by many other languages. All its warts are known. It is easier to implement and more people are familiar with it and its pitfalls.
If inheritance is not an option, how about scrapping them off? Some modern languages such as Rust choose to not have inheritance at all. It means there is no automatic method promotion, and you actually have to rewrite the methods one by one. Despite the seemingly tedious endeavor, in practice, it is actually quite pleasant to write in Rust. Does having embedded type provide significant values over not having one?
Finally, there is this question whether Go 2.x needs to retain compatibility with Go 1.x? If you want to retain compatibility with Go 1.x, then there is really no point in continuing this discussion. There is no way you can get rid of the embedded type while retaining compatibility with Go 1.x. Again, this highlights the complexity of the embedded type feature. Once it logs into your code, it is difficult to refactor it out. You can't just write tools to do it. You have to do it manually one by one, judging the case for each one.
Comment From: bcmills
Perhaps Go's embedded type behavior is satisfying another type of relationship that we are not familiar with?
How about incorporates-a?
You wouldn't embed a Dog
in a Human
struct, because the dog is not a part of the human.
On the other hand, you might embed a Horn
in a Car
struct: a car has-a horn, but because the horn is part of the car it is still sensible to say “there are a lot of cars honking on this street”.
Why not use the conventional inheritance? It is a path well-trodden by many other languages. All its warts are known.
You've answered your own question here. 🙂
Its warts are known, and we think (hope?) that Go's approach will lead to better code. (Not problem-free, but perhaps with less severe problems.)
Finally, there is this question whether Go 2.x needs to retain compatibility with Go 1.x?
That is an open question. (See https://blog.golang.org/toward-go2.)
Comment From: DeedleFake
Some modern languages such as Rust choose to not have inheritance at all. It means there is no automatic method promotion, and you actually have to rewrite the methods one by one.
But Rust does still offer an alternative. You can use traits to grant methods to types as long as those traits are in scope. For example:
trait Example {
fn do_something(&self);
}
impl<T: SomeOtherTrait> Example for T {
fn do_something(&self) {
}
}
And voila. All types that implement SomeOtherTrait
now also automatically implement Example
as long as Example
's definition is in scope. I've also put together a more full example in the Rust Playground.
I'm not advocating Rust's way of doing it, athough I do think it can be pretty neat sometimes. I'm just saying that Rust also has ways to give methods to multiple types at a time. You may also be able to do something similar using enums, but I'm not exactly sure.
Comment From: henryas
Thanks for the Rust example. You actually don't need two traits. You can merge them into one. The feature at play is the default method. Rust allows you to define default methods for your interface. It allows "method promotion"-like behaviour. The difference between Rust's and Go's approaches is that, in Rust, "Derived.Action()" always mean the derived object doing the action, whereas in Go, "Derived.Action()" is actually "Derived.Base.Action()". It is the base object doing the action, but you can write it as "Derived.Action()" which, depending on the context, may be misleading. The question is whether this behaviour has any merit. Hence, this is the main issue that the proposal is trying to address.
Comment From: KamyarM
Well, I have a better idea for you. Let's remove struct totally. Who needs a struct or OOP at all!! let's write codes like 40 years ago.
Comment From: dsnet
I have no judgements regarding whether embedding should be added or removed, but I do find the current specification of them somewhat unsatisfactory. Here are few experience reports.
Embedding can lead to forward incompatible code
I have seen cases where a user embeds a protobuf struct into their own struct because they wanted to have easy access to the struct fields in the protobuf message. However, doing so inadvertently causes their type to also satisfy the proto.Message
interface because all of the protobuf methods are being forwarded. This is wrong because their newly defined type is not a proto message.
As a general principle, using embedding of an external type in a forwards compatible way requires the user to think about all of the possible methods the embedded type may end up adding. My observation is that many programmers lack the discipline to see far ahead about the implications.
For example, suppose you embed a type that currently has methods M1, M2, ..., Mx that you want to forward. In the future, if the embedded type adds a String() string
method, then your type now forwards the String
method, when you most likely did not want it to be forwarded.
Embedding interacts poorly with pointers and unexported types
The fields of an embedded struct are forwarded even if the embedded type is unexported. This can have unexpected behavior when combined with pointers. For example:
type s struct { X int }
type S struct { *s }
x := S{new(s)}
x.X = 5 // This is actually accessible from external packages
However, this pattern is strange in that while S.X
is accessible from external packages, it is not settable if the s
struct is not first allocated. However, an external package is not able to allocate S.s
since it an unexported field. Thus, the following panics:
x := S{}
x.X = 5 // panic since s is not allocated
This is the root of an inconsistency in the json
package. See #21357.
Embedding concrete types to achieve extensible interfaces
This is not what embedding is designed for, but something it is being used for.
In Go, interfaces cannot be extended without breaking all concrete implementation (since they do not implement the newly added method). Thus, the following pattern can be used to achieve that:
// Users implementing Interface must embed the DefaultInterface struct type.
type Interface interface{
Method1(...)
Method2(...)
// We may need to add methods here in the future.
embedDefaultInterface() // unexported method prevents accidental satisfication
}
type DefaultInterface struct { ... }
func (d *DefaultInterface) Method1(...) {}
func (d *DefaultInterface) Method2(...) {}
func (d *DefaultInterface) embedDefaultInterface() {}
At a later point, Interface.Method3
can be added without fear of breaking all implementations, so long as we add a default implementation as DefaultInterface.Method3
. However, this is still inferior to something like default methods in Java interfaces because the methods of the embedded type can only call DefaultInterface
methods and not the version defined by custom concrete implementations.
While this does achieve some degree of interface extensibility, it is still unsatisfactory.
\cc @neild
Comment From: pciet
Struct embedding is one of my favorite Go features but if there was a better solution I’d be happy with that. The simplicity it provides is great:
type ReduceCase struct {
Set
Out Set
}
type Piece struct {
Kind
Orientation
Base Kind
Moved bool `json:"-"`
}
type Point struct {
*Piece // nil for no piece
AbsPoint
}
Comment From: pciet
driver.Drive(s60.Volvo.Car.Vehicle)
can be reduced to driver.Drive(s60)
: https://play.golang.org/p/tILZH-UUJGZ
Comment From: DeedleFake
@pciet This seems like one of those super-contrived examples (class Car {}; class Volvo extends Car {}
) that only comes up as an example and, if you ever run into it in practice, is a pretty good indicator that the entire API needs heavy redesigning. I'm generally in favor of keeping struct embedding, but I don't think this is a good example to demonstrate why with.
For this particular example, with no other context, it would probably be a better idea to make Vehicle
an interface, since it's there to provide a guarantee of functionality, not a specific layout of data. You could embed Vehicle
in a Car
interface, and then make Volvo
an implementation of Car
, and thus one of Vehicle
at the same time. Then just make Drive()
take a Vehicle
argument.
Comment From: as
@DeedleFake
Most of those suggestions are already in the example... too bad there is no diff
for comments <-> examples.
it would probably be a better idea to make Vehicle an interface
It already is an interface
You could embed Vehicle in a Car interface
Ah, so here's where the idea is.
Then just make Drive() take a Vehicle argument.
It does.. sort of. There are two Drive functions, one is a method. This is where I agree that the API gets weird.
Comment From: pciet
@DeedleFake I agree that this is a contrived example, but my point was that Go doesn’t require something like s60.Volvo.Car.Vehicle
.
Changing Car to interface would make every instance of a car a type which doesn’t make sense to me.
Comment From: NDari
Reading through this discussion, I wanted to see for myself what everyone is talking about. So I wrote this code.
type Foo struct { V int }
func (f Foo) Say() {
f.V += 1
f.MakeNoise()
}
func (f Foo) MakeNoise() { fmt.Println("I am a foo!") }
type Bar struct { Foo }
func (b Bar) MakeNoise() { fmt.Println("I am a bar!") }
type Baz struct { Foo }
func (b Baz) Say() {
b.V += 1
b.MakeNoise()
}
func main() {
var b Bar
b.MakeNoise()
fmt.Println(b.V)
b.Say()
fmt.Println(b.V)
b.V += 1
fmt.Println(b.V)
b.Say()
fmt.Println(b.V)
var t Baz
fmt.Println(t.V)
t.Say()
fmt.Println(t.V)
t.V += 1
fmt.Println(t.V)
t.Say()
fmt.Println(t.V)
}
I was not expecting the results of this. The fact the Bar.V is distinct from Bar.Foo.V was not clear to me: For example, calling Bar.Say() does not change the Bar.V. I was also surprised that the value of Bar.Foo.V was also unchanged after calling Bar.Say(). This does not make any sense to me.
Being confused, I changed all the function receivers to be pointers to the respective types, link to playground and now the value of *.V changes in the way that I would expect. I hope that I am not the only one confused by the output of at least one of the print statements in the above two programs.
Comment From: henryas
Another thing that bothers me about embedded struct is the lack of encapsulation. Let's say we have the following data type:
type Mercedes struct {
Vehicle
}
You can invoke the method with both mercedes.MoveForward()
and mercedes.Vehicle.MoveForward()
. Mercedes does not need to expose access to Vehicle. mercedes.MoveForward()
is enough. If one day Mercedes decides to change its implementation to derive its MoveForward method from Sedan, it's going to break everybody that uses mercedes.Vehicle.MoveForward()
. There is no reason why you need access to Vehicle.
Comment From: cznic
> There is no reason why you need access to Vehicle.
Comment From: bcmills
@NDari I think the source of your confusion — pointer vs. value receivers — is mostly independent of embedding. (See https://golang.org/doc/faq#methods_on_values_or_pointers.)
It may be that embedding exacerbates the problem (for example, by encouraging people to think in misleadingly object-oriented terms), but I don't think removing embedding would significantly reduce confusion about value receivers in general.
Comment From: NDari
@bcmills You are correct. I revisited the example and my confusion is orthogonal to the issue of embedded as you said.
Comment From: dsnet
Here's another example of the poor interaction of embedding and the visibility rules: #24153.
Comment From: dsnet
24466 is an example of a user who seems to be using embedding to forward the fields of a struct, but does not intend to forward the methods.
Comment From: ianlancetaylor
There is some good discussion above about problems using embedded fields. What I don't see is any discussion of alternatives other than manually repeating all the methods and renaming all field references. Embedded fields are used today and they do serve a purpose. If we are going to eliminate them, we need to have something to replace them with.
Leaving open for now for further discussion. This proposal can't be accepted without some idea of what to replace embedded fields with. Simply eliminating them from the language, although it would simplify matters, can't work with the large existing base of Go code.
Comment From: henryas
If the automatic method fallover is deemed necessary, I would like to propose the reduction of embedded struct inner object's visibility as an alternative solution:
Given the following:
type Car struct {
FuelLevel int
}
func (c *Car) Drive(distance int, consumptionRate int){
///implementation..
}
type Mercedes struct {
Car
}
mercedes.FuelLevel = 30 //OK!
mercedes.Drive(100, 1) //OK!
//from within the same package
mercedes.Car.FuelLevel = 30 //still okay
mercedes.Car.Drive(100, 1) //still okay
//from external package
mercedes.Car.FuelLevel = 30 //no longer legal
mercedes.Car.Drive(100, 1) //no longer legal
//you need Mercedes to do all the work if you are accessing from external package
mercedes.FuelLevel = 30 //OK!
mercedes.Drive(100, 1) //OK!
//Of course, method override still work as normal. Mercedes may override the Drive method.
mercedes.Drive(100, 1) //if overridden, it calls Mercedes' overridden version.
mercedes.Car.Drive(100, 1) //not legal when accessed from external package.
//if Mercedes needs to allow access to its Car object, it must explicitly allow it to happen.
func (m Mercedes) Car() Car {
return m.Car
}
//then, from external package we can do this if necessary.
mercedes.Car().Drive(100, 1)
//the key here is that Mercedes has complete control over access to its inner object.
The problem with the current implementation of embedded struct is that it exhibits both an "is-a" and an "has-a" relations. What the above solution does is to make a distinct separation of the "is-a" from the "has-a". Embedded struct is an "is-a". Mercedes is a car and you can perform car-related activities directly on the Mercedes. There is no need to access the Mercedes' Car field.
If you need a "has-a", you can do the following:
type Person struct {
MyCar Car
}
person.MyCar.FuelLevel = 30 //OK
person.MyCar.Drive(100, 1) //OK
Now, this Person has a Car.
Another advantage of this approach is that it should be relatively easy to fix existing code using gofix.
Comment From: as
The problem with the current implementation of embedded struct is that it exhibits both an "is-a" and an "has-a" relations.
Can you explain why? Where's the logic in that conclusion that supports this assertion? "is-a" and "has-a" are just alphabet soup.
I see no advantages to the change--it just breaks compatibility with existing code. It's common to wrap a type in another type and override just one or two methods, leaving the others forwarded. With your change, it becomes impossible to call the inner object's original method: highly undesirable.
Comment From: henryas
Can you explain why? Where's the logic in that conclusion that supports this assertion? "is-a" and "has-a" are just alphabet soup.
person.DoWork()
and person.Car.DoWork()
read differently. The first implies you are asking the person to do the work. The latter implies you are asking the person's car to do the work. They have different meanings to the reader, and the current embedded struct allows you to have both as if they are the same and let you change one with the other.
Note that the alphabets soups you mentioned are pretty much taught in programming courses and textbooks. They even have it in Wikipedia. While I appreciate the skepticism towards modern programming practices, it doesn't mean that you should do the opposite everything they do just for the sake of doing it. Not all of them are bad.
Personally, I still think that embedded struct is not needed. I don't use it in my work. However, I have never found myself in a situation where I need to write 30+ methods on a single object, and then write the exact 30+ methods for several other objects. So I wouldn't know the value of automatic method propagation to others.
Ian mentioned that writing forwarding methods take much work and that automatic method propagation has its values. Hence, if method propagation is all that is needed, we can still have it but with some usage clarification (syntax-wise) to avoid confusion to the reader, whether you mean person.DoWork()
or person.Car.DoWork()
.
You brought up some good points about needing to access the inner object. I realize that internally you may still need to refer to the inner object, but externally you don't. Hence, I have updated my earlier post about the alternative solution.
Comment From: jozuenoon
No, no to this change.
1) embedding standard library structs/interfaces like sync.Mutex
,
2) overriding functionalities in existing structures especially from standard library,
3) testing / mocking
Comment From: MatejLach
With my current Go project, which requires implementing an OO-heavy W3C standard, (ActivityPub), I rely heavily on struct embedding to simplify the implementation. Removing embedding might simplify the compiler, but complicates the implementation of many other types of programs. I don't think we should simplify one program, (the compiler), at the expense of complicating many other programs.
I'll be in favour of an alternative of course, but not just an outright removal as is. Once there are interface constrained generics and interfaces are more akin to Rust traits, I can see embedding being unnecessary, but not in isolation.
Comment From: bilus
Isn't much of the discussion needlessly based on looking at a Go feature through lenses of OOP? Inheritance is not the only way to cut the cake and the fact that a language feature can be used to emulate inheritance doesn't mean it is inheritance.
It doesn't even make sense from OOP perspective because if embedding is-a inheritance then why does it break the base concept's contract? Or, the other way around, it breaks the contract, therefore it isn't inheritance. ;)
Jokes aside, there are other languages that support embedding (e.g. Clojure records) but do not have the syntax sugar Golang has. I personally find marginally useful in the kind of projects I'm using Go for but it doesn't make it anything more than just syntax sugar.
Asking to remove the syntax sugar because it does an imperfect job when you stretch it emulate an OOP construct is just ... On the other hand, the points that @dsnet is making are pretty practical.
Comment From: metakeule
@henryas @ianlancetaylor
Why not have an explicit forwarding shortcut instead to replace the automagic? That way we could have all the benefits of embedding without the strong implicit coupling.
e.g.
type Person struct {
Name string
Age int
Children []Person
}
type Programmer struct {
Age int
Skills []string
FavoriteLanguage string
}
func (p *Programmer) HasSkill(skill string) bool {
for _, sk := range p.Skills {
if sk == skill {
return true
}
}
return false
}
type ProgrammingParent struct {
Person Person
Programmer Programmer
}
forward *ProgrammingParent -> .Programmer(Skills,FavoriteLanguage,HasSkill)
forwardAll *ProgrammingParent -> .Person
Syntax could be improved, just to demo the idea.
Comment From: vincent-163
Why not have a type specifically designed for embedding? This might work best with templates. Here is a rough idea of how it works:
template Car
func (c Car) Drive() {
c.Fuel -= 10 // it does not have to be a field that Car has already defined, it just needs to exist in the struct that uses the template
}
type Mercedes struct {
Fuel int
} @Car // Mercedes specifies that it uses the Car template
// Now Mercedes has a method called Drive
Basically, you define things with a special keyword like "template" that's like a set of methods and fields that extend something else. Or if it is really a template it can wrap something else. It still lacks a decent syntax that is clear and simple but I believe it will work well.
Comment From: henryas
I would like to add that another problem with embedded type is the automatic exported field visibility. When you embed type A into type B, type A is directly accessible from type B. It is like two features (Visibility and Embedded Properties) rolled into one.
In my opinion, it is better to have the visibility part separated from the embedded part. It is more orthogonal that way. Just imagine if changing your monitor's brightness also changes your speaker's volume. Unless that is exactly what you want, it is just awkward.
Should you decide to keep embedded types in Go2, my proposed syntax for unexported embedded type is as follow:
type A struct{}
func (A) DoSomething(){}
type B struct{
_A //unexported embedded type A
}
var b B
b.DoSomething() //Okay!
b._A = A{} //ILLEGAL: cannot access b._A
My stance is still to consider removing embedded type from Go2. It makes Go2 a lot simpler and it compensates the additional complexity due to extra features that have accumulated since Go1.0.
Comment From: lnashier
@ianlancetaylor I'll see what I can do.
@as It's actually pretty cool to be a fire-breathing human once in a while. But how much I will stay as a human depends on my earlier expectation of my Parents. If I thought that my Parents were already humans and therefore I didn't implement much humanity in me (because it was already implemented by my Parents), when my Parents turn into a giant lizard, I too will turn. Basically, I don't become what I expect myself to be, but I am actually the enhanced version of my Parents (whatever my Parents want to be). You may argue that this is technically the correct definition of inheritance (and it is), but it breaks the original assumption about me. The assumption about me depends on my assumption about my Parents, whereas my Parents don't necessarily know (and don't need to!) my assumption about them.
The main issue is that I don't encapsulate my Parents trait in me. When my client deals with me, they also have to deal with my Parents, and possibly my Grandparents as well. It creates unnecessary coupling and breaks encapsulation. Changes in one will have a domino effect on the others. With proper encapsulation, the effect of a change should be contained like ship compartmentation.
Again, it depends on whether this is the intended behavior that the Go's Team wants in Go language. If it is then I say they should not hesitate and support the proper inheritance all the way. After all, despite its criticism, inheritance is still a major feature in many popular programming languages today. If they do not want it, then they should scrap it and I vote for scrapping it.
You chose wrong parents. Isn't it cool that you can choose the parents. :-)
Joke aside, they are more like step-parents, they must keep the contract. Your fear is, what if they may break the contract. If that's troubling you, Go talk to them.
Comment From: ibraheemdev
@henryas @ianlancetaylor
Why not have an explicit forwarding shortcut instead to replace the automagic? That way we could have all the benefits of embedding without the strong implicit coupling.
e.g.
```go type Person struct { Name string Age int Children []Person }
type Programmer struct { Age int Skills []string FavoriteLanguage string }
func (p *Programmer) HasSkill(skill string) bool { for _, sk := range p.Skills { if sk == skill { return true } } return false }
type ProgrammingParent struct { Person Person Programmer Programmer }
forward ProgrammingParent -> .Programmer(Skills,FavoriteLanguage,HasSkill) forwardAll ProgrammingParent -> .Person ```
Syntax could be improved, just to demo the idea.
I really like this idea. The delegate
method in ruby on rails could also be looked at for implementation:
class Greeter < ActiveRecord::Base
def hello
'hello'
end
end
class Foo < ActiveRecord::Base
belongs_to :greeter
delegate :hello, to: :greeter
end
Foo.new.hello # => "hello"
Manually forwarding methods to a struct field is unnecessarily verbose and also introduces a problem with test coverage.
Comment From: YamiOdymel
I was going to disagree the existence with embedded struct BUT I realized it's useful if you are designing APIs.
It will be lot easier to make Endpoint APIs from different perspectives with the same Resource since you can just extend it with extra information for frontend needs.
// The resource that stores in the database.
type User struct {
ID int
Username string
}
// ...
type GetUserResponse struct {
User
// Extra information for this API only.
IsBlocked bool
}
// ...
type GetRelationshipResponse struct {
User
// Extra information for this API only.
FriendedAt time.Time
MutualFriends int
}
Without embedded structs, you gonna either having a duplicated structs:
// ...
type GetUserResponse struct {
ID int
Username string
IsBlocked bool
}
// ...
type GetRelationshipResponse struct {
ID int
Username string
FriendedAt time.Time
MutualFriends int
}
Or include User
struct as a property
and ended up like GraphQL with nested structs:
// ...
type GetUserResponse struct {
User User
IsBlocked bool
}
// ...
type GetRelationshipResponse struct {
User User
FriendedAt time.Time
MutualFriends int
}
Comment From: jub0bs
@henryas If you embed an exported type into a struct, you indeed leak information about the composition of the embedding type. However, one extra line gets you out of all the difficulties you mentioned regarding the lack of information hiding. You can indeed define a local, unexported type alias of the type you wish to embed, and embed that type alias instead:
type Foo struct{}
func (Foo) DoSomething(){}
type foo = Foo // <--------------
type Bar struct {
foo
}
var bar Bar
bar.DoSomething() // Okay!
bar.foo = Foo{} // ILLEGAL: anonymous field foo is inaccessible outside Bar's package
This is essentially the same idea as that proposed by @cznic in his comment, but it uses a type alias rather than a defined struct type.
Comment From: henryas
This proposal has been derailed into syntax hack and workaround. The problem is not the syntax, but the very idea of embedding itself.
Concept-wise, when one embed A into B, what is the relationship between A and B? Is it an “is-a” where B is an A? Is it a “has-a” where B is composed of A?
If it is an “is-a” where B is an A (eg. Alice is a Person), embedding does not work quite as well as a proper inheritance in other languages. The forwarded methods from A still refer to A instead of B. Quite a number of people in Golang-Nuts try to use embedding to simulate this relationship and got confused when things do not work as expected. For instance, there is one that tries to embed Animal into Cat. Animal has a Serialize method and the method gets promoted into Cat. So you can call Cat.Serialize(). However, when you call Cat.Serialize(), you are essentially calling Cat.Animal.Serialize() because it serializes the Animal and not the Cat. So the question is why do you need to promote the Serialize method into Cat in the first place since it has nothing to do with the Cat? In this respect, embedding cannot be used to sufficiently describe the “is-a” relationship.
If it is a “has-a” relationship (eg. Alice has Eyes), embedding becomes awkward because of method promotion. One can already describe such relationship without embedding. You can call Alice.Eyes.Color without needing embedding. With method promotion it becomes awkward because Alice.Color does not refer to Alice’s color but it refers to her eyes’ color.
In addition, the field and method promotion aspect of embedding results in increased coupling. The author of A may add fields and methods which may be relevant to A but no longer relevant to B. Inheritance is often viewed in a bad light because of a similar situation. When you have a long hierarchy of types, all it takes is just one bad API change that gets propagated down the lines and break the whole types in the group. At least in the case of Inheritance, Inheritance sufficiently explains the “is-a” relationship while it is not clear what kind of relationship Embedding is describing.
People nowadays shun a long-hierarchy of interdependent types. Rust does not have inheritance to discourage this practice. Go’s embedding does nothing to discourage this. You may not like the term inheritance and you may say Go has composition and not inheritance, but Go’s embedding has all the inheritance bad charactertistics and none of its good ones.
To me, embedding seems more like a convenient tool to copy-and-paste fields and methods of A into B, but then it may be clearer to have B.A.Characteristics() without embedding at all. Embedding is like one weird vocabulary that you do not know where to use. With IDEs, text-editors, code generators, etc., saving a few keystrokes seems like a bad excuse.
In addition, without embedding, the compiler and other go tools may have easier and simpler ways to do their jobs. There is no magic between structs.
At the same time, I recognize that Go 2 may require backward compatibility with Go 1 and there are people who use embedding all over the place. So it may not be practical to remove embedding, unless if Go has ways to deprecate features. This is why I do not pursue this proposal with such passion anymore, but I thought that this could be a good food for thoughts.
Comment From: jub0bs
@henryas As others before me have pointed out, you're mistaken about inheritance vs. composition. Go does not have inheritance, period. Via named struct fields, you get basic composition. Embedding simply adds automatic method and field promotion to the outer type.
Are you implying that we should get rid of embedding simply because it's not the right approach in all cases? If so, your argument makes no logical sense to me. Of course, there are cases where embedding is not what you want. If you do not want automatic method and field promotion to the outer type, do not use embedding; simply use composition via a named struct field, and selectively delegate some of the methods if needed. However, there are plenty of cases where embedding is convenient and reduces boilerplate. See the source code of the context
package, for just one example.
the field and method promotion aspect of embedding results in increased coupling. The author of A may add fields and methods which may be relevant to A but no longer relevant to B.
That is a valid point, and I agree that people shouldn't use embedding willy-nilly. For instance, I don't think embedding a third-party type is necessarily a good idea. Like all features of a language (reflection?), embedding can be abused—I'm actually drafting a blogpost on the topic. However, you shouldn't throw the baby out with the bathwater.
it may be clearer to have
B.A.Characteristics()
without embedding at all
But wouldn't this approach result in the same kind of coupling that you were decrying above? Clients of the B
type could break as soon as A
changes.
Embedding is like one weird vocabulary that you do not know where to use.
I respect your point of view, but you can't speak for the entire community.
Comment From: sircodemane
I don't agree with removing embedding, but I do think that the discussion it created has made me believe that we should focus on re-thinking how we view embedding and teaching new patterns around it.
A lot of the discussion here has focused on the "is-a" versus "has-a", which corresponds to inheritance versus composition. I think the problem is trying to make embedding fit strictly into the inheritance or composition boxes, when it is actually something different than both, and should be taught as such:
A is B because it has C
, where A is a composed struct, B is an interface, and C is a component Struct. A can do B things because the C component was bolted on. Imagine this use case where Car
/Phone
is a Device
because it has a Battery
and Meta
:
package main
type BatteryPowered interface {
Charge(int)
Deplete(int)
GetCharge() int
}
type Named interface {
GetName() string
}
type Device interface {
Named
BatteryPowered
}
type Battery struct {
charge int
}
func (b *Battery) Charge(amount int) {
b.charge += amount
}
func (b *Battery) Deplete(amount int) {
b.charge -= amount
}
func (b *Battery) GetCharge() int {
return b.charge
}
type Meta struct {
name string
}
func (m *Meta) SetName(name string) {
m.name = name
}
func (m *Meta) GetName() string {
return m.name
}
type Car struct {
Meta
Battery
}
type Phone struct {
Meta
Battery
}
func UseDevice(device Device) {
println("using " + device.GetName())
device.Deplete(24)
}
func main() {
car := Car{}
car.SetName("Mercedes")
phone := Phone{}
phone.SetName("Pixel")
UseDevice(&car)
UseDevice(&phone)
}
Go does not support inheritance in any way, so Car and Phone will never be a Battery or a Meta. Go does support composition, but we have to differentiate composition from embedding because classical composition simply describes that Car and Phone have a battery property(component), where embedding means that Car and Phone do have a battery, but because we embedded it, we intentionally wanted Car and Phone to do BatteryPowered things. And since we added Meta to Car and Phone, they are also Named, and being both Named and BatteryPowered means they are a Device.
I admit that the example is arbitrary and contrived, but the idea is that embedding is not truly inheritance or composition, and the main problems arising from it is trying to perceive it as either. Such is the case with Alice and eye color. If we apply this slight change in our way of thinking to that scenario, it becomes obvious that embedding Eyes into Person doesn't quite make sense: "Person is Colorful because it has Eyes". Instead, Eyes should be a component property of Person, distinguishing a clear difference between embedding and classic composition.
Comment From: henryas
@codydbentley Your example could be reduced to the following. It is done without embedding and it ends up being a lot simpler. There are fewer data types. They are flat and have fewer dependencies.
type Battery struct {
charge int
}
func (b *Battery) Charge(level int) {
b.charge += level
}
func (b *Battery) Deplete(level int) {
b.charge -= level
}
func (b Battery) GetCharge() int {
return b.charge
}
type Car struct {
name string
battery Battery
}
func (c Car) Name() string {
return c.name
}
func (c *Car) SetName(name string) {
c.name = name
}
func (c *Car) Battery() *Battery {
return &c.battery
}
type Phone struct {
name string
battery Battery
}
func (p Phone) Name() string {
return p.name
}
func (p *Phone) SetName(name string) {
p.name = name
}
func (p *Phone) Battery() *Battery {
return &p.battery
}
type BatteryPowered interface {
Name() string
Battery() *Battery
}
func UseDevice(device BatteryPowered) {
fmt.Printf("using %s\n", device.Name())
device.Battery().Deplete(24)
}
func main() {
car := Car{}
car.SetName("Mercedes")
car.Battery().Charge(100)
phone := Phone{}
phone.SetName("Pixel")
phone.Battery().Charge(100)
UseDevice(&car)
UseDevice(&phone)
}
Looking back, my early Go codes use plenty of embedding. Being familiar with other OO languages, I used embedding mostly to simulate inheritance. Soon I learned that embedding does not work like inheritance at all. Like inheritance, embedding has method propagation. Unlike inheritance, the propagated methods do not refer to the 'child' object. On the other hand, composition does not need method propagation. It ends up being weird if used with method propagation. I agree with you that embedding is neither an "is-a" nor a "has-a". It is like a half-hearted implementation of inheritance and I cannot find the words to describe the relationship that embedding conceptually describes.
As I gain experience with Go, my later codes do not use embedding. I do not intentionally avoid the feature. I just do not find it useful. Things are simpler without embedding.
However, as I said before, it may not be practical to remove embedding. Plenty of codes will be broken, including some that I wrote during my early Go adventure and the libraries that I still use now. So I suppose we should just let embedding stay for backward compatibility purposes.
Comment From: willfaught
Go's embedded types are prototype-based inheritance, like what JavaScript has.
In JavaScript, when you assign parent
to myobj.prototype
, then access an attribute myobj.attr
, the runtime will first look for attr
on myobj
, then on parent
, then on parent.prototype
, and so on until the prototype chain is exhausted.
Go types can have a list of prototypes, instead of just one, as embedded types. Instead of resolving Go's version of attributes, methods, at runtime, they're resolved at compile time. It's the same idea.
Go has inheritance. It's always had inheritance (just like exceptions). It just doesn't call it that. It has prototype-based inheritance, not class-based inheritance. The whole point of inheritance is to reuse code. We use prototype-based inheritance to lift existing code into new implementations of interfaces, without the inflexible straitjacket of class-based inheritance.
Comment From: andewx
@bcmills The problem with automatic method implementation is that the people who write
BaseDriver
may not know the context in whichBaseDriver
are used by their derived drivers, and may inadvertently break the original assumption about the specific drivers. For instance, in your example, I may have certain drivers whom I may want them to pay toll using cash card that the company can control, rather than with cash. I may already haveEnterTollRoad(card *CashCard)
method in my driver. The changes you make to theBaseDriver
just provided a way to circumvent the security measure I have in place for my specific driver.
All I know is that Go by trying to be "dead-simple" in its, "we're not an object oriented language, but we have objects approach", created somewhat of an issue with code reuse by being obtuse about the best way to handle extensions.
Inheritance in my opinion circumvents alot of these issues if you just allowed the types to inherit and extend functionality, it's just more straight-forward, and most inheritance hierarchies should only go 1-2 levels deep.
But in Go the solution was stated above by embedding interfaces, which is just a roundabout away of getting polymorphism and extensibility.
But as for the forwarding methods solution, you're generating a lot of wrapper interfaces for handling every composition, do you really want to do that. If you do that's fine but you the developer or team always has the option to say this is our style and implementation. And I like that Go has those options.
In my opinion my judgment is the opposite, introduce inheritance as another optional feature.
type Truck from Car{
tow_capacity float32
//....
}
Comment From: willfaught
Go has a form of prototype-based inheritance, like JavaScript, except instead of being limited to one inherited value, there can be many (embedded structs). This was intentional, although perhaps the Go designers didn't think of it as "inheritance". Go inheritance (code reuse) uses composition: you inherit a method set from an embedded type, then intermingle your own methods among them. The inherited methods are implemented by the embedded value. driver.Drive(s60.Volvo.Car.Vehicle)
is how it's supposed to work, because driver.Drive only knows how to handle concrete vehicle types. If you want driver.Drive to handle an interface instead, then just do that, and have vehicles implement it.
Comment From: henryas
Go has a form of prototype-based inheritance, like JavaScript, except instead of being limited to one inherited value, there can be many (embedded structs). ...
There is a significant difference between Javascript's inheritance and Go's "inheritance". In Javascript, the keyword this refers to the current object. In Go, the method receiver refers to the originating object. In the case of method inheritance, the Javascript's version will refer to the child object, while the Go's version will refer to the parent object.
Inheritance has been a mainstay in the programming world for decades and many people are familiar with how inheritance works. When people look at Go's embedding, they start thinking "Oh! This is inheritance", and there is a danger in treating Go's embedding as inheritance. Go's embedding is a crippled version of inheritance. It is best to think of it as a tool to copy and paste fields and methods from one struct to another. Don't attach any other meaning to it (eg. whether a Student is a Person, or a Student belongs to a Class). To the unwary, Go's embedding can be a pitfall.
When it comes to solving inheritance problem, Go's dynamic interface does a better job in flattening the tall hierarchy issue than embedding does. In fact, embedding encourages some sort of hierarchy. Just like inheritance, with embedding, the definition of a type may get split up across several types and modules, leading to a fragmentation of code logic. You want a banana, but you must find the gorilla holding the banana and the jungle holding the gorilla, etc.
There is also a visibility issue where it is impossible to make the embedded struct private.
In short, embedding has all the inheritance warts, solves none of them, and does not offer the inheritance's full benefits. It is also a non-essential feature that you can live without. I am sure, implementation-wise, it is also complex and presents certain challenges for future language changes. I can imagine the amount of work involved to get generics to play nicely with embedding.
I understand that it is impossible to remove embedding while maintaining backward compatibility with Go1. I just want to point out that embedding is the least valuable feature in Go.
Comment From: BenjamenMeyer
I understand that it is impossible to remove embedding while maintaining backward compatibility with Go1. I just want to point out that embedding is the least valuable feature in Go.
I'd disagree. It's quite a valuable feature in Go alongside Interfaces, and unlike Inheritance it allows for some better clarity of the scope and methods.
That is, since attaching methods to a type requires specifying the type being worked on, it's clear what the scope is - whether pointer receiver or value receiver even.
About the only wart is determining which methods take priority when multiple embedded types have the same method attached - but that's easily resolved by attaching a new method on the new type to determine the behavior, and even explicitly call the appropriate attachments for the embedded types - they can still be individually accessed and resolved just by scoping what is being worked on.
Comment From: willfaught
There is a significant difference between Javascript's inheritance and Go's "inheritance". In Javascript, the keyword this refers to the current object. In Go, the method receiver refers to the originating object. In the case of method inheritance, the Javascript's version will refer to the child object, while the Go's version will refer to the parent object.
I don't understand what you're saying here. In JavaScript, "this" refers to the head of the inheritance list. In Go, the receiver refers to the root of the inheritance tree. If a method isn't defined on the head/root, then the ancestors are searched for the first match. It's the same idea.
Inheritance has been a mainstay in the programming world for decades and many people are familiar with how inheritance works. When people look at Go's embedding, they start thinking "Oh! This is inheritance", and there is a danger in treating Go's embedding as inheritance.
Go takes great pains to distance itself from how things are done in other languages, like class-based inheritance and exceptions, even eschewing those terms as much as possible. There are no "extends" or "inherits" keywords. When someone is new to Go, they should learn the language, not blindly assume that everything is the same as another language. https://go.dev/ref/spec is freely available to all.
Go's embedding is a crippled version of inheritance. It is best to think of it as a tool to copy and paste fields and methods from one struct to another. Don't attach any other meaning to it (eg. whether a Student is a Person, or a Student belongs to a Class). To the unwary, Go's embedding can be a pitfall.
When it comes to solving inheritance problem, Go's dynamic interface does a better job in flattening the tall hierarchy issue than embedding does. In fact, embedding encourages some sort of hierarchy. Just like inheritance, with embedding, the definition of a type may get split up across several types and modules, leading to a fragmentation of code logic. You want a banana, but you must find the gorilla holding the banana and the jungle holding the gorilla, etc.
Go does not have explicit type hierarchies. The point is to focus on method sets, and how to assemble them, not on taxonomies. Go uses prototype-based inheritance to allow you to reuse implementations by assembling them into larger components that implement interfaces. This avoids the general problem that class-based inheritance shares with global variables: it's difficult to reason about how variable "layers" interact with each other in "both" directions.
There is also a visibility issue where it is impossible to make the embedded struct private.
You can embed an unexported type.
In short, embedding has all the inheritance warts, solves none of them, and does not offer the inheritance's full benefits. It is also a non-essential feature that you can live without.
So is class-based inheritance. C was the biggest language in the world for a long time. Go is a descendent of C.
I understand that it is impossible to remove embedding while maintaining backward compatibility with Go1. I just want to point out that embedding is the least valuable feature in Go.
I don't agree. Complex types are. ;)
Comment From: jub0bs
@willfaught I appreciate most of your latest post but I keep thinking that you should use the term prototype-based inheritance more carefully. JavaScript supports prototype-based inheritance. Go supports subtyping (via interfaces) and composition, but no form of inheritance whatsoever. Claiming otherwise will likely just breed more confusion.
Comment From: willfaught
@jub0bs I linked to the definition of prototype-based inheritance, and explained the analogy to JavaScript. What specifically do you disagree with?
Just because the language doesn’t call it prototype-based inheritance, doesn’t mean it doesn’t have it; it’s an abstract concept. I identified it by name to respond to the argument that Go should have class-based inheritance: Go already has inheritance, of a different kind.
Comment From: jub0bs
@willfaught You write
Just because the language doesn’t call it prototype-based inheritance, doesn’t mean it doesn’t have it [...]
Terms matter. We should agree on their precise meaning before intelligible discourse can take place. In particular, we shouldn't conflate two radically different concepts.
Go simply has no concept of prototype as understood in the JavaScript world. The Go specification does not contain the words "prototype" or "inheritance". The word "inherit" appears only once, but in the negative:
A defined type may have methods associated with it. It does not inherit any methods bound to the given type [...]
The Go FAQ states there's no type inheritance and mentions nothing about prototype-based inheritance (or class-based inheritance, for that matter).
If those authoritative resources don't sway you, I don't know what will...
Comment From: willfaught
I agree that terms matter, but I disagree that it's wrong or confusing to correctly use an abstract term to refer to something usually called by another name. Go has "generics", but in computer science type theory, it's called parametric polymorphism. Both terms have been used interchangeably in discussions about the design of Go's generics. It's not improper, or confusing, to use one term over the other.
It's the same case here. Go has prototype-based inheritance, but it calls it struct embedding. If you don't like that particular correct terminology, then don't use it or ignore it, but don't try to stop others from using it. As I said above, I only introduced this particular term to explain that Go already has inheritance, so adding class-based inheritance would not be orthogonal. In any other context, I wouldn't have even brought it up.
The Go FAQ states there's no type inheritance
They're talking about class-based inheritance. 99% of the people who want inheritance, like the OP, want class-based, and don't even know about prototype-based as an option. So they geared the Q&A around that. As I suggested above, even the Go team might not have realized that struct embedding is a kind of prototype-based inheritance, which could also explain why it's not addressed there.
If those authoritative resources don't sway you, I don't know what will...
I've already addressed this point.
Comment From: kidmoe667
I agree with willfaught's assertion ... given how Go's embedded types work, things behave much like a prototype based inheritance. I don't see how there is any imprecision or inaccuracy with such an observation.
For fun, some while ago I attempted to mimic JavaScript's prototype behavior in Python and Julia ... since both languages give the programmer some control on how an object's "API" works. You can build objects via successive composition ... but then recursively forward methods. The easiest implementation is that you terminate the recursion upon finding the first "match" ... just as willfaught has already discussed.
While certainly one can claim that Go doesn't possess traditional class-based inheritance ... certainly, the observation can still be made that embedded-types do, in fact, permit a type of inheritance that is prototype-like (how's that for a change in terminology).
Comment From: isocroft
I agree with neither the technical accuracy of the premise nor the final conclusion. Inheritance locks in functional dependencies in the base class. Embedding allows those dependencies to be overridden or propagated at a per-method granularity.
There's nothing wrong with class-based inheritance. No one has been able to prove to me that something is wrong with it. Inheritance isn't evil. It's only evil because of the way we use it. A lot of software engineers start out by creating parent classes first before sub classes which is how inheritance can easily go wrong as it is akin to premature abstraction and the abuse of the DRY principle. One should take a bottom-to-top approach by creating sub classes first before creating parent classes as this allows you to focus on only the logic/behavior and data boxes you require immediately.
Read more here
So, while i do not support removing struct embeddings going forward (for Go v2) as the OP has agreed to not pursue such. I feel that avoiding it entirely leads simpler code.
Comment From: hanserya
Language newcomer here. However, I will posit that Go definitely does not implement inheritance, prototypal or otherwise. It simply uses embeddings to achieve polymorphism through composition.
Inheritance and composition are two different tactical approaches to achieve polymorphism. Another way to look at it would be to consider that polymorphism is a semantic relation within your programs while sub classing (inheritance) and embeddings (composition) are syntactic language features that allow you to achieve that state of operability.
There is no inherent benefits to either approach over the other. Ultimately it is a matter of preference. The GOF guidance that seems to form the basis for Go's implementation details is even stated as a preference rather than a general rule. The problems that I'm seeing argued throughout this thread are actually Liskov violations that can be committed within either paradigm. The bottom line is that language features don't make good OO software developers. Rather, good OO programmers understand how to employ language features correctly.
With that being said, the only pro/con of this proposal that I can see is as follows:
Pro: adding inheritance reduces barriers to entry for OO developers that hail from other stacks. More devs usually equates to a healthier and more vibrant ecosystem.
Con: adding inheritance increases the complexity of the language. There's now more than one way to "skin the cat" when it comes to polymorphism in the Go language.
While there's no real counterargument to the pro, there is one to the con. Technically, the language could include inheritance as a way of introducing simpler subtyping graphs. The current mechanism of embeddings allows for something akin to multiple inheritance. While admittedly a powerful language feature, it can absolutely be maddening in the wrongs hands. Subclassing can be enforced in isolation within project teams that don't want to open that can of worms.
Additionally, classes can be used to "disable" the automagic of embeddings for similar reasons. Case in point: I'm currently on a quest to determine how the compiler addresses name collisions between methods/attributes on multiple embeddings within the same struct. I only happened upon this thread while researching that topic because it actually isn't called out in the literature that I've read so far. I'm sure that there's and answer but I imagine it'll either be some esoteric convention or a outright compiler error. There may also be a way to resolve the name collision when they occur. In any case, single inheritance through subclassing ensures that such scenarios never occur in the first place - which may be appropriate for some teams.
Regardless, I don't see any wisdom in just ripping out embeddings entirely. They are not inherently evil in any way. The problems that they've been blamed for either: exist in inheritance schemes as well; are really a complaint about "magic" language features in general; or, are actually attributable to the support for multi embeddings rather than to embeddings in-and-of themselves.
I'd actually propose that subclassing be implemented as design time syntax sugar that the compiler translates into embeddings and avoid changes to the runtime instructions entirely - but that's just me.
Comment From: ianlancetaylor
As noted, this change would not be backward compatible. We have no plans to ever change Go such that existing programs stop working. Therefore, this is infeasible. Closing.