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: ability to create and import methods for non-local types #21401
Comments
This allows two different packages to add different implementations of the same function to a type. For instance:
How does |
I have already addressed this in the original proposal:
|
When and how is that error reported? Also, if I extend If it won't, then if I pass |
It's the same rules as when you declare two functions with the same name in a given package, or do an
Does the file that defies the Print function import that extension? Does it also check for
See my previous answer. The extension is scoped to the file that imports it, it does not taint the type for subsequent packages/files.Thus, nothing unexpected will happen. |
Well, no, because those examples are all reported as errors by the compiler. In Keith's example above, the compiler be invoked on each of those packages separately, so it will never see the erroneous code. To be clear, I'm asking a question about implementation here. I understand that this is an error according to your proposal. I'm asking how it can be implemented.
I think it's quite odd that the same exact value with the same type would behave differently merely because I call, say
defined in some different package. |
In the above example, a panic would be produced, since
I'm sorry, I don't quite get how this example ties in with the proposal. |
My comment about |
@ianlancetaylor |
I would also like to add an addendum to the proposal: Essentially, since it's dynamic type features the new interface, passing it to an external functions that expects such an interface is a valid call. If we go back to the |
OK, given a value
? That is, I convert |
If that expression is within the scope of the extension, then this conversion will success, since the first part will be an On the other hand, if v was created where the extension was in scope, then passed to a scope without the extension, it depends what the argument type was. If the argument's type was |
Thanks. It seems to me that this behavior is confusing. An type has a method set, and that has a clear meaning in conjunction with interface types. With this proposal, the association of the method set and interface types depends crucially on how the values are passed around. To me that seems like an undesirable feature. |
@ianlancetaylor From the standpoint of the current Go spec, my proposed behavior seems anything but confusing. |
This seems loosely inspired by C#'s extension methods. If Go did want to do something in this space (making function calls chainable), then another potential source of inspiration is Javascript's proposed "::" operator. I could imagine making the expression The strings example would then look like this:
I've always thought extension methods are a neat idea, as they allow libraries to define a small core interface (IEnumerable in .NET's case) and have "fake methods" on that interface to allow a relatively expansive and ergonomic fluent API. I imagine a hypothetical future Go implementation of Apache Beam, or a hypothetical Go2 (with generics) implementation of functional transformations (map/reduce/filter/etc) would all benefit from such a structure. That being said, with either proposal, I think the standard library would probably need to be updated. With the original proposal, to rewrite packages to make use of this functionality, or with the "::" mini-proposal, to possibly reorder parameter lists to put a logical element in the first position. |
Your The Another option would be to provide the traditional right-to-left compose operator, perhaps in conjuction with a placeholder variable: func Actions(actions []string) {
strings.Replace(_, "alpha", "beta", -1) ∘
strings.Replace(_, "desc", "name", -1,) ∘
strings.Join(actions, "\n")
} |
@bcmills |
|
@as |
We'll have to agree to disagree on readability with respect to including packages in names. In my opinion, naming the source of functions is a good thing (and easily avoided in both current-Go and a hypothetical future-Go, with a local With respect to your second goal: I found it quite strange that interface satisfaction changes depending on which package a call-site is in. Currently, the result of the expression Here's the tc39 proposal for the "::" operator, for the record: https://github.com/tc39/proposal-bind-operator#examples. (Only the first two examples are relevant to Go, since Go already has its own syntax for method extraction) |
If I understood you correctly, you're worried that since
While this is very similar, my problem is that we already have a syntax for defining methods for a type, and then for calling them. So instead of reusing that syntax, bringing it to its logical conclusion, this introduces another operator |
For the particular case of Do you have concrete use-cases for the proposed feature other than the |
@bcmill, iirc, it was stated that the reason the built-in types don't have methods is because that would complicate the spec, as those methods would have to be written there (i believe this was stated in an AMA on reddit). As for use cases from the stdlib itself, Then you have third party libraries. Some, like |
Also, as method sets are not inhereted it would create strange situations
like this
type Method string
var Get Method = "GET"
None of the methods on "GET" would be available on Get.
…On Fri, 8 Sep 2017, 16:55 Viktor Kojouharov ***@***.***> wrote:
@bcmill,
iirc, it was stated that the reason the built-in types don't have methods
is because that would complicate the spec, as those methods would have to
be written there (i believe this was stated in an AMA on reddit).
As for use cases from the stdlib itself, strings and bytes are the
no-brainers. Then there's math and its subpackages. IMHO, the API of
math/bits can be greatly simplified if it defined methods for
uint{-,16,32,64} instead of having methods like Len{-,16,32,64}. strconv
functions can be distributed to their types. unicode could extend rune.
If we assume Go2 will also come with generics, the stdlib should also come
with packages with helpful map and slice functions (map/reduce/filter/etc
for anyone that needs them).
Then you have third party libraries. Some, like github.com/pkg/errors,
can benefit substantially by this. Others, like the various utility
functions (I assume) we all write will be of benefit for our own projects.
Examples of these would be the functions that obtain a value from a context
and map it to a certain type, and these are nowadays quite predominant.
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
<#21401 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AAAcA9vMU2idxV7-L4xSgkmdq11ixZd7ks5sgOT-gaJpZM4O0z7R>
.
|
Calling the functions from the From that standpoint, putting the methods on the type seems like a strict improvement: then the author of the package could choose to embed it to retain the methods. type Method struct{ string }
var Get = Method{ "GET" } |
That may be so, but it seems like an appendix of
The
That's an interesting case, because
The The
That one is a weird case because the operands are already interfaces, not concrete types. What happens if the underlying values already implement one of the methods you would be adding? |
I don't think it's ambiguous. The
The current rules for type aliases state that any methods defined for type T1 would be available for the alias type T2. I would say that not following this rule would be horribly confusing.
If
From the point of view of |
I am new to Go but would like to chime in on a real use case that I am trying to solve. The issue is that I am implementing a library (say "Serialize") for custom serialization of types. Say my library is used in order to serialize types that are obtained from another library (say "X"). I expect the glue code to teach my library how to serialize X types and then pass these instances directly to my library. So say I make an interface like:
Then I want the glue code to call X's functions to obtain instances and just be able to run Serialize.Serialize(X.MakeInstance(....)) where X.MakeInstance() returns one of the instances in X (note that X has no idea their objects will be serialized so they may return say an instance of:
In other words it is not enough for the glue to cast X.XYZ to a delegate type because this will not convert embedded types (which may be returned by the serializer). I would really like to be able to define this in glue code:
The best solution I came up with so far is to design my own type dispatcher system (which is certainly possible). So glue code can register specific serializers for different types in X and the dispatcher can select the correct serializer by asserting to the required type (or using the reflect package). But this seems a bit clunky (although this is what I ended up doing in python for the same purpose). When I first read about Go's method dispatch system it seemed exactly like what I was doing by hand and so seemed perfect - only to be disappointed by learning that it was not allowed to add interfaces to external types from other packages. It seems to me that this restriction makes the Go type system no different from traditional class based OO designs because methods can not be added to types outside the package they are defined in making the types non-maliable - if I want to make a type conform with a new interface I need to extend it (just like a class based design) or embed it (making me explicitly dispatch all the methods I care about) and this does not even solve the problem of receiving the old type from external sources since I will need to cast those to the new type somehow. |
@scudette I'm not sure what you mean by dispatch in this context, but embedding a type in another type silently forwards all the public methods as long as the embedding is anonymous. |
Caveat: A Go newbie coming from predominately C++ and Python background so maybe when I am saying here does not make any sense :-). @as I guess I see that embedding a type in a struct anonymously is the same as deriving from a base class (since all methods are automatically forwarded, the type can grow extra methods and can even override methods from the embedded type). I was referring to dispatch as non-anonymous embedding which in other OO languages would be composition and would require explicit delegation. It was just a misunderstanding on my part - after reading the Go documentation which describes the type system as being very different from other OO languages with method dispatch instead of class hierarchy I misunderstood it. IMHO the restriction of attaching methods to types only within their type definition packages basically boils the type system into being a traditional class based language with a different syntax but not fundamentally different:
Is equivalent to (albeit with a stricter type system):
And embedding is equivalent to subclassing:
Is the same as:
Since Bar has both Method() and OtherMethod() but this does not solve the original problem of what to do when someone hands you a Foo instance and you need to support the interface by attaching the OtherMethod() to it. Now we have to do a non-trivial copy operation to convert a Foo to a Bar just so we can appease the type system that the object complies with the interface. It would be much easier to just add the interface to the Foo class without having to modify the base class code or extend it, which IIUC is the proposal in this issue. I agree this can get very confusing if the baseclass already has this method (which one should win?) but this is easy to detect during compilation and linking, and in practice probably wont matter because if two modules are trying to support a protocol on a type they should (theoretically) have the same implementation. |
Presumably an extended method is not able to access unexported fields or methods of the type. You can already create a new type with additional methods by using type embedding: type New struct { pkg.Old }
func (n *New) NewMethod() { ... } The additional complexity required in the spec and implementation to implement this proposal does not seem justified by the incremental gain of functionality beyond what you can already do in the language. |
Correct me if I'm wrong, package slices
func Map(type T1, T2)(s []T1, f func(T1) T2) []T2 {...}
func Reduce(type T1, T2)(s []T1, initializer T2, f func(T2, T1) T2) T2 {...}
func Filter(type T)(s []T, f func(T) bool) []T {}
s := []int{1, 2, 3}
// I have made some small changes here.
s1 := slices.Map(s, func(i int) int { return i + 1 })
evens := slices.Filter(s1, func(i int) bool { return i%2 == 0 })
sum := slices.Reduce(evens, 0, func(i, j int) int { return i + j }) Make the functions in sum := slices.Reduce(
slices.Filter(
slices.Map(
s, func(i int) int {
return i + 1
}),
func(i int) bool {
return i%2 == 0
}),
0, func(i, j int) int {
return i + j
}) The readability is worse than the original codes which have to create temporary variables to save intermediate results. s := []int{1, 2, 3}
// x |> f is syntactic sugar for f(x).
sum := s |> func(e []int) []int {
return slices.Map(e, func(i int) int {
return i + 1
})
} |>
func(e []int) []int {
return slices.Filter(e, func(i int) bool {
return i%2 == 0
})
} |>
func(e []int) int {
return slices.Reduce(e, 0, func(i, j int) int {
return i + j
})
} With light weight anonymous function, sum := s |> ((e) => slices.Map(e, (i) => i + 1)) |>
((e) => slices.Filter(e, (i) => i%2 == 0)) |>
((e) => slices.Reduce(e, 0, (i,j) => i + j)) With a placeholder variable: sum := s |> slices.Map( _, _ + 1) |>
slices.Filter( _, _ %2 == 0) |>
slices.Reduce( _, 0, _ + _ ) The strings example would then look like this: import "strings"
func Actions(actions []string) {
actions |> strings.Join( _, "\n") |>
strings.Replace( _, "desc", "name", -1) |>
strings.Replace( _, "alpha", "beta", -1)
} |
This proposal is to allow method definitions for types, which are not create in the current package.
The current go spec specifically forbids defining methods for a type, unless the type is defined in the same package as its methods. This proposal is for relaxing these rules in certain cases.
We will then be able to use
"my string".Index(...)
from anywhere within packagefoo
When importing an extension, a list of types must be provided, corresponding to the methods one wishes to import. Importing multiple definitions of a method for a given type is an error. Moreover, since this is a type of import, there is no need to provide an import declaration for the same package, unless one wishes to use something else that package provides, like extra types/functions.
Example:
Similarly to import declarations, extension declarations are valid for the file that uses them.
The incentive behind implementing this proposal is two-fold. First, when compared with the usability of packages like 'strings', it allows for more fluent and readable APIs:
vs.
Second, it allows existing to support new interfaces by defining the missing methods. This is currently doable by defining a new type using the current one as an underlying type, and then delegating all the existing methods to the underlying type, which is itself quite tedious.
The text was updated successfully, but these errors were encountered: