Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

proposal: Go 2: extend types out of package where it is defined. #37742

Closed
leaxoy opened this issue Mar 8, 2020 · 13 comments
Closed

proposal: Go 2: extend types out of package where it is defined. #37742

leaxoy opened this issue Mar 8, 2020 · 13 comments
Labels
Milestone

Comments

@leaxoy
Copy link

leaxoy commented Mar 8, 2020

In modorn language, like rust, swift, dart, programers can extends types in user code. In rust, it's impl Foo for Bar, in swift, it's extension Foo, in dart, it's extension Foo on Bar. The extension syntax give ability to extend any types in a simple way. It's powerful and useful. But now in Go, we can't achieve this any way.
In Rust way:

// this is user mod
impl ToInt for String {
    fn to_int(&self) -> Result<isize, Error> {}
}

In Swift way:

extension Int {
    func as_double() double {
    }
}

In Dart way:

extension NumberParsing on String {
  int parseInt() {
    return int.parse(this);
  }
  // ···
}

Existing way.

The existing way to achieve this is define new types or embed types in another types.

1. Define new types. For example:

in package a we have a type A:

package a
type A struct {}

in package b, if we want extend a.A, we must define a new types

package b
type B a.A

and then:

func (b B) Foo() {
}

but when using a.A, we must transform to b.B, it's annoying and not required if we have the ability to extend types out of package where it is defined.

2. Another way is embed in another type

in package a

package a
type A struct {
    ...
}

and in package b

package b
type B struct {
    a a.A
    // or 
   a.A
}

then

func (b B) Foo() {
    b.a.XXX()
    // or
   b.XXX()
}

it's also need wrap a.A to b.B to give an extended method Foo().


Proposal way

At this background, I proposal add ability to extend types out of package where it's defined. There are some syntax.

1. Normal syntax without any breaking syntax.

package b

import "a"

func (a *a.A) Foo() {

}

the only change is *A -> *a.A.

2. Add new keyword like extend or ext.

package b

import "a"

extend func (a *a.A) Foo() {}

extend func (a *a.A) Bar() {}

the extend keyword before func is to describe method Foo and Bar is extension method.

3. Like 2 add new keyword but place it before method name.

package b

import "a"

func (a *a.A) + Foo() {}

func (a *a.A) + Bar() {}

the + means this method is additional.


Usage

The above syntax give some way to achieve this, the following will give example how to use it.

1. use old _ import syntax

package main

import (
    "a"
    _ "b"
)

// now we can use `a.A.Foo`
func main() {
    var a a.A
    a.Foo()
}
@gopherbot gopherbot added this to the Proposal milestone Mar 8, 2020
@leaxoy leaxoy changed the title proposal: Go 2, extend types out of package where it is defined. proposal: Go 2: extend types out of package where it is defined. Mar 8, 2020
@ianlancetaylor ianlancetaylor added v2 A language change or incompatible library change LanguageChange labels Mar 8, 2020
@ianlancetaylor
Copy link
Contributor

This has been suggested before, though I can't locate any earlier conversations.

The problem is interface types. If a type is defined in package a, adding a method in package b can mean that the type implements interfaces that it did not implement before. That means that conversions to interface type will change. When should it change? Should all types created in package a now implement the new interface? How should conversion in and out of interface{} work? How should type reflection work?

@beoran
Copy link

beoran commented Mar 8, 2020

Moreover, this makes methods hard to locate. If I have a foo.Bar them now I can be sure that all methods of Bar are defined in the package foo, and that there can be no others.
As you showed yourself, the way to do this in Go is to make new types. This is "the right thing" to do in such a situation, and if other languages don't enforce this, I would say those languages get it wrong.

@urandom
Copy link

urandom commented Mar 8, 2020

Previous discussion
#21401

@urandom
Copy link

urandom commented Mar 8, 2020

Perhaps a different take to solve this would be to build on top of the underlying type syntax.

In Go, you can create a type from an underlying type, but lose the underlying type's methods:

type Foo bar.Bar

You can also create a type alias which "inherits" the methods of the "underlying" type, because that is not really a new type:

type Foo = bar.Bar

For discussion's sake, what about creating new types from an underlying type, which also "inherit" that type's method, and perhaps, implicit conversion between the new type and extended type can also be allowed:

type Foo + bar.Bar

Foo will be a completely new type that way, so there won't be any gotchas.

@leaxoy
Copy link
Author

leaxoy commented Mar 9, 2020

This has been suggested before, though I can't locate any earlier conversations.

The problem is interface types. If a type is defined in package a, adding a method in package b can mean that the type implements interfaces that it did not implement before. That means that conversions to interface type will change. When should it change? Should all types created in package a now implement the new interface? How should conversion in and out of interface{} work? How should type reflection work?

This is indeed a problem! Can we create a virtual type for reflect and expose user the original type, this is just an idea.

@leaxoy
Copy link
Author

leaxoy commented Mar 9, 2020

Moreover, this makes methods hard to locate. If I have a foo.Bar them now I can be sure that all methods of Bar are defined in the package foo, and that there can be no others.
As you showed yourself, the way to do this in Go is to make new types. This is "the right thing" to do in such a situation, and if other languages don't enforce this, I would say those languages get it wrong.

Maybe we do some trade off, just refer rust implementation, rust can add new method in mod a to String, user code just do use crate::a::SomeTrait to give ability to String type.
In this case, we must explicitly import package a to give ability to string or SomeType. Like the usage section shows.

@leaxoy
Copy link
Author

leaxoy commented Mar 9, 2020

Perhaps a different take to solve this would be to build on top of the underlying type syntax.

In Go, you can create a type from an underlying type, but lose the underlying type's methods:

type Foo bar.Bar

You can also create a type alias which "inherits" the methods of the "underlying" type, because that is not really a new type:

type Foo = bar.Bar

For discussion's sake, what about creating new types from an underlying type, which also "inherit" that type's method, and perhaps, implicit conversion between the new type and extended type can also be allowed:

type Foo + bar.Bar

Foo will be a completely new type that way, so there won't be any gotchas.

Thanks for the suggestions, but all of them have limits.
The first solution, new type will lose original method.
Second one, type alias is just an alias of original type, we can't add new method on it, this not solve type extension problem.
Last one, at first glance it seems feasible. But in my opinion, + can be replaced by with, because + means the same type in both left, right side, with means additional property or constraint.

@ianlancetaylor
Copy link
Contributor

@urandom Thanks for finding the earlier discussion.

@leaxoy Is there anything new in this issue that is not covered by the discussion in #21401?

@maj-o
Copy link

maj-o commented Mar 14, 2020

@urandom this thing with + would mean: if I "derive" my type from e.g. int, I could use operators like + in my "derived" type? - I don't think so. Unless operators are handled like methods or interfaces.
@leaxoy Go is not object-orientated in this way, Go is more likely method-orientated. In Go You would simple implement your interface, regardless which type (struct) implements this interface (or better said: for which class this interface is implemented), as long as the method simply exists - it works. For instance the interface Reader. It works very well. If You want to extend it (The Go way is! Do not change the the class (struct), but the targeted interface), You may introduce an interface ReadWriter (Which also already exists but is a good example and works very well) - at this point - You have Your wished extension. You simply declare an interface (old interface (Reader) and Your "extension" (Writer) and get ReadWriter). The main goal of Go is, that every class implementing this can be used at interface points. Great isn't it? - So Your extended class can be used and every other class Implementing the same interface (maybe a different database backend or even a greater change). It will simply work, if the underling methods work (what is simply to test).

@urandom
Copy link

urandom commented Mar 14, 2020

@maj-o you already can use operators that work on your underlying type with your type

@ianlancetaylor
Copy link
Contributor

Based on the discussion about this is similar to #21401 which was closed. Therefore this is a likely decline. Leaving open for four weeks for final comments.

@neakor
Copy link

neakor commented Apr 7, 2020

Would love to have this.

@ianlancetaylor
Copy link
Contributor

No change in consensus. Closing.

@golang golang locked and limited conversation to collaborators Apr 7, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests

7 participants