Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

proposal: spec: type intersection and composition #71641

Closed
4 tasks
meftunca opened this issue Feb 10, 2025 · 4 comments
Closed
4 tasks

proposal: spec: type intersection and composition #71641

meftunca opened this issue Feb 10, 2025 · 4 comments
Labels
LanguageChange Suggested changes to the Go language LanguageChangeReview Discussed by language change review committee Proposal
Milestone

Comments

@meftunca
Copy link

Go Programming Experience

Experienced

Other Languages Experience

Typescript

Related Idea

  • Has this idea, or one like it, been proposed before?
  • Does this affect error handling?
  • Is this about generics?
  • Is this change backward compatible? Breaking the Go 1 compatibility guarantee is a large cost and requires a large benefit

Has this idea, or one like it, been proposed before?

No

Does this affect error handling?

No

Is this about generics?

No

Proposal

This proposal suggests adding a mechanism to Go's type system that allows developers to define new types by combining existing types. This could be achieved in one of two primary ways:

Type Intersection: A new type would be created that possesses all the fields and methods of the constituent types. This would be analogous to intersection types in TypeScript or similar languages. For example:

Go

type Address struct { Street string; City string }
type Person struct { Name string; Age int }

type UserInfo = Address & Person // Proposed syntax
UserInfo would then have the fields Street, City, Name, and Age.

Type Composition (or Embedded Structs 2.0): This would build upon Go's existing embedding mechanism but potentially offer more explicit control. It might involve a new keyword or syntax to clearly indicate the intent of composing types. While embedding already allows for composition, a more explicit approach could address some of its current limitations (e.g., name clashes). An example (using a hypothetical composes keyword):

type Address struct { Street string; City string }
type Person struct { Name string; Age int }

type UserInfo struct {
    composes Address
    composes Person
}
// or
type UserInfo struct {
    composes Address
    composes Person
    Email string
    Location string
//...
}

This, similar to the intersection approach, would result in UserInfo having the fields Street, City, Name, and Age. The key difference would lie in how name collisions are handled and potentially in how methods are promoted.

Who does this proposal help, and why?

This proposal would benefit Go developers in several ways:

Reduced Code Duplication: Currently, if you need a type that combines fields from multiple structs, you often have to manually copy those fields into a new struct definition. This leads to redundant code, which is harder to maintain and prone to errors. Type intersection/composition would eliminate this duplication.
Improved Code Readability: The intent of combining types would be expressed directly in the type declaration, making the code easier to understand and reason about.
Enhanced Type Safety: By explicitly defining the combined type, the compiler could enforce type correctness, preventing common errors related to missing or mismatched fields.
More Expressive Type System: The addition of type intersection or composition would make Go's type system more powerful and flexible, allowing developers to model complex data structures more effectively.
This would be particularly helpful in scenarios involving:

Data Modeling: Representing complex entities that are composed of multiple, simpler entities (e.g., a customer with an address, contact information, and purchase history).
API Design: Creating flexible and reusable data structures for APIs.
Code Reusability: Combining existing types to create new types without rewriting code.
Please describe as precisely as possible the change to the language.

Language Spec Changes

Adding type intersection or composition would necessitate changes to the Go Language Specification in several key areas:

Type Declarations: The most obvious change would be the introduction of new syntax within type declarations. This could involve:

Intersection: Introducing the & operator within type declarations to signify intersection. For example:

type UserInfo = Address & Person

Composition: Introducing a new keyword (e.g., composes) or modifying the existing embedding syntax to explicitly denote composition. For example:

type UserInfo struct {
    composes Address
    composes Person
}

Type Identity and Assignability: The spec would need to define how the new composite types are treated in terms of type identity and assignability. Specifically:

When are two composite types considered equal? (e.g., same constituent types, same order, etc.)
What are the rules for assigning values of one type to another involving the new composite types?
Field and Method Promotion: The spec would need to detail how fields and methods from the constituent types are "promoted" to the new composite type. This would include:

How are name collisions handled? (e.g., if both Address and Person have a String() method)
Which methods are accessible on the composite type? (e.g., all from both, or only those from one?)
Scope and Visibility: The spec might need to clarify how the scope and visibility of fields and methods are affected by the new composite type.

Embedding (If Applicable): If the composition approach builds upon embedding, the spec might need adjustments to reflect the new keyword/syntax and any resulting changes in behavior compared to current embedding.

Compiler and Tooling: While not strictly spec changes, the implementation of this feature would require corresponding updates to the Go compiler (gc), go vet, and other related tools.

Reflection: The spec might need to outline how reflection interacts with the new composite types. (e.g., what information is available via reflect.TypeOf, etc.)

Informal Change

"Alright class, today we're going to talk about something Go doesn't currently do, but some of us think it should. It's about combining types, kind of like how you might mix ingredients to make a new dish.

Imagine you have two structs: one for Address and one for Person. Address has Street and City, and Person has Name and Age. Now, what if you want to represent a UserInfo that has all of this information? Right now, in Go, you'd probably do something like this:

type Address struct {
    Street string
    City   string
}

type Person struct {
    Name string
    Age  int
}

type UserInfo struct {
    Address Address
    Person  Person
}

// Or, you might copy the fields individually:

type UserInfo struct {
    Street string
    City   string
    Name   string
    Age    int
}

See how we either embed the structs or manually copy the fields? That's okay, but it can get a bit tedious, especially if you have lots of fields or lots of structs you want to combine. Plus, if you change Address later, you have to remember to update UserInfo too!

So, the idea we're exploring is to make this process easier and more expressive. We're thinking about adding a way to directly combine types, almost like saying, "Hey Go, I want a new type called UserInfo that's basically an Address and a Person all rolled into one."

We have two main approaches in mind:

  1. Type Intersection (like a Venn diagram overlap): Imagine two circles, one for Address and one for Person. The overlapping part in the middle? That's what type intersection would give you. We might use a symbol like & to represent this:
type UserInfo = Address & Person // Hypothetical syntax!
This would automatically create a UserInfo type with all the fields from both Address and Person.
  1. Type Composition (like building with Lego blocks): This is similar to how embedding works now, but we might make it more explicit. Imagine you have Lego blocks for Address and Person. Composition would be like snapping them together to build a UserInfo block. We might introduce a new keyword like composes (or tweak embedding) to make this clear:
type UserInfo struct {
    composes Address // Hypothetical syntax!
    composes Person
}

Again, this would give you a UserInfo type with all the fields.

Now, there are some details we'd need to figure out. What if both Address and Person have a field called Location? How do we handle that? We'd need rules for resolving these "name collisions." Also, how would methods work? If both types have a String() method, which one does UserInfo use?

This is what we're trying to figure out! We're not saying this will be added to Go, but we want to discuss it, explore the possibilities, and see if it's a good fit for the language. It's all about making Go even better!"

Is this change backward compatible?

Yes, because this feature just adds a new typing flexibility to the language.

Orthogonality: How does this change interact or overlap with existing features?

No, actually the purpose of this feature is to enable developers to set up project data structuring in a more organized way. I need it to reduce workload and complexity by combining the same fields in an easy way instead of defining them over and over again in multiple fields.

Would this change make Go easier or harder to learn, and why?

I don't think it will affect the learning speed of go very much.

Cost Description

The cost of this feature is that it is likely to force GoLang to be more inclined to include typing features like those found in most modern languages. For example, other users may request “typescript” features from you. Golang is a very powerful language, but it is a bit behind in some areas. Especially with typing, I find it very difficult to make things as flexible as I expect.

Changes to Go ToolChain

I don't know

Performance Costs

I don't know

Prototype

Scenario 1: User Profile

Imagine you have separate structs for User and Profile:

type User struct {
    ID    int
    Name  string
    Email string
}

type Profile struct {
    Bio      string
    Location string
    Website  string
}

With type intersection/composition, you could create a UserProfile type that combines both:

// Using hypothetical intersection syntax:
type UserProfile = User & Profile

// Or, using hypothetical composition syntax:
type UserProfile struct {
    composes User
    composes Profile
}

Now, UserProfile would have all the fields: ID, Name, Email, Bio, Location, and Website.

Benefit: Avoids redundant field definitions, promotes code reuse.
Scenario 2: Data Transfer Object (DTO)

Suppose you have a database model and you want to create a DTO for your API:

type Product struct {
    ID          int
    Name        string
    Description string
    Price       float64
}

type ProductDTO struct {
    Name        string
    Description string
}

You could use composition to include the relevant fields:

type ProductDTO struct {
    composes Product // Includes Name and Description
}

Benefit: Keeps DTOs concise and focused, reduces the risk of inconsistencies between models and DTOs.
Scenario 3: Event Handling

Let's say you have different event types:

type ClickEvent struct {
    X int
    Y int
}

type KeyEvent struct {
    Key string
}

You could create a generic Event interface and then use composition to define specific event types:

type Event interface {
    // ...event methods
}

type ClickEvent struct {
    composes Event // Inherits from Event interface
    X       int
    Y       int
}

type KeyEvent struct {
    composes Event // Inherits from Event interface
    Key     string
}

Benefit: Enables more structured and organized event handling, promotes type safety.
Scenario 4: Embedding for Behavior

Go's embedding is already a form of composition, but let's illustrate how methods can be implicitly "promoted":

type Logger struct {
    Prefix string
}

func (l *Logger) Log(message string) {
    fmt.Println(l.Prefix + ": " + message)
}

type User struct {
    Logger // Embedded Logger
    Name   string
}

func main() {
    user := User{
        Logger: Logger{Prefix: "USER"},
        Name:   "Alice",
    }
    user.Log("User created") // Call Log method on embedded Logger
}

Benefit: Allows types to inherit behavior from other types, facilitating code reuse and a more object-oriented style (although Go is not strictly an OO language).
Important Notes:

These examples use the hypothetical composes keyword for illustration. The actual syntax would need to be defined.
Name collisions (when two composed types have the same field or method name) would need to be handled.
The specific semantics of method promotion and type identity would need to be carefully considered.

@meftunca meftunca added LanguageChange Suggested changes to the Go language LanguageChangeReview Discussed by language change review committee Proposal labels Feb 10, 2025
@adonovan adonovan changed the title Feature Request: Add Support for Type Intersection or Composition in Go proposal: spec: type intersection and composition Feb 10, 2025
@gopherbot gopherbot added this to the Proposal milestone Feb 10, 2025
@seankhliao
Copy link
Member

This proposal doesn't answer any of the interesting questions as to why existing embedding isn't sufficient, or how it'd be different. Rather than try to intersect fields, it would probably be better to just define common struct and embed that into the places that need them.

While this proposal has some syntax, there doesn't appear to be anything real to evaluate.

@seankhliao seankhliao closed this as not planned Won't fix, can't repro, duplicate, stale Feb 10, 2025
@meftunca
Copy link
Author

I'm sorry, but the type concatenation feature is not as small as you make it out to be. For most developers, this kind of feature will help them write better quality and more comfortable code. Below I share a few examples of areas where it can be used

Current usage

type UserDepartmentsRK struct {
	UserId       types.URID `json:"userId" gorm:"primaryKey;column:user_id;type:uuid;"`
	CompanyId    types.URID `json:"companyId" gorm:"primaryKey;column:company_id;type:uuid;"`
	DepartmentId types.ID   `json:"departmentId" gorm:"primaryKey;column:department_id"`
}

type UserDepartmentsForm struct {
	UserId       types.URID `json:"userId" gorm:"primaryKey;column:user_id;type:uuid;"`
	CompanyId    types.URID `json:"companyId" gorm:"primaryKey;column:company_id;type:uuid;"`
	DepartmentId types.ID   `json:"departmentId" gorm:"primaryKey;column:department_id"`
	Supervisor   *bool          `gorm:"column:supervisor;nullable" json:"supervisor,omitempty" validate:"omitempty" swaggertype:"boolean"  example:"true" `
	TimeCreated  types.NullTime `gorm:"column:time_created;nullable;autoCreateTime" json:"timeCreated,omitempty" validate:"omitempty" swaggertype:"string"  example:"1960-09-17T02:37:51Z"  format:"date-time"`
}

Planned usage

type UserDepartmentsRK struct {
	UserId       types.URID `json:"userId" gorm:"primaryKey;column:user_id;type:uuid;"`
	CompanyId    types.URID `json:"companyId" gorm:"primaryKey;column:company_id;type:uuid;"`
	DepartmentId types.ID   `json:"departmentId" gorm:"primaryKey;column:department_id"`
}


type UserDepartmentsForm struct UserDepartmentsRK & {
	Supervisor   *bool          `gorm:"column:supervisor;nullable" json:"supervisor,omitempty" validate:"omitempty" swaggertype:"boolean"  example:"true" `
	TimeCreated  types.NullTime `gorm:"column:time_created;nullable;autoCreateTime" json:"timeCreated,omitempty" validate:"omitempty" swaggertype:"string"  example:"1960-09-17T02:37:51Z"  format:"date-time"`
}

@ianlancetaylor
Copy link
Member

With the "inline" option in #63397, you should be able to do that with embedding.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
LanguageChange Suggested changes to the Go language LanguageChangeReview Discussed by language change review committee Proposal
Projects
None yet
Development

No branches or pull requests

5 participants