New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
proposal: Go 2: remove embedded struct #22013
Comments
(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. |
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 Looking at your alternative, which you wrote as:
Imagine the realistic case where 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). |
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. |
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. |
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 |
@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:
You have two alternatives. First, you can use the real composition and employ method forwarding, which would be as follows:
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:
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. |
It does not behaves like inheritance at all. Given type T int
func (T) foo()
type U struct {
T
}
var u U
|
@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. |
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. |
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
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. |
That claim is not obvious to me. Could you give some concrete examples? |
@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. |
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 |
@bcmills The problem with automatic method implementation is that the people who write |
@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. |
@ianlancetaylor if you read my earlier posts, I did point out that ... well, I will summarize it again here:
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. |
@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 . |
@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:
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. InheritanceWhat 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.
Now, what if we changed the Parent to be a bird.
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.
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:
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? CompositionNow, 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:
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:
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. ConclusionLet'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. |
It is not.
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. |
@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. |
@davecheney I am puzzled.
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?
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? |
@cznic yup, that's about as clear as I can make it
A A |
There is no relationship. |
This is not correct, |
Go supports this already, at the risk of sounding sarcastic, these are just |
@bcmills You are correct. I revisited the example and my confusion is orthogonal to the issue of embedded as you said. |
Here's another example of the poor interaction of embedding and the visibility rules: #24153. |
#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. |
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. |
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. |
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. |
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 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. |
No, no to this change.
|
With my current Go project, which requires implementing an OO-heavy W3C standard, (ActivityPub), I rely heavily on struct embedding to simplify the implementation. 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. |
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. |
Why not have an explicit forwarding shortcut instead to replace the automagic? 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. |
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. |
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. |
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. |
I really like this idea. The 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. |
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 // ...
type GetUserResponse struct {
User User
IsBlocked bool
}
// ...
type GetRelationshipResponse struct {
User User
FriendedAt time.Time
MutualFriends int
} |
@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. |
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. |
@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
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.
But wouldn't this approach result in the same kind of coupling that you were decrying above? Clients of the
I respect your point of view, but you can't speak for the entire community. |
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:
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. |
@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.
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. |
Go's embedded types are prototype-based inheritance, like what JavaScript has. In JavaScript, when you assign 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. |
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:
You can do this
Let me know what you think.
Thanks.
Henry
The text was updated successfully, but these errors were encountered: