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: Add new type designed to aggregate small interfaces #29467

Closed
mikeschinkel opened this issue Dec 30, 2018 · 19 comments
Closed

proposal: Go 2: Add new type designed to aggregate small interfaces #29467

mikeschinkel opened this issue Dec 30, 2018 · 19 comments
Labels
FrozenDueToAge LanguageChange Proposal v2 A language change or incompatible library change
Milestone

Comments

@mikeschinkel
Copy link

I have written up a proposal for Contracts in Go on my new blog.

I wrote it there before realizing others have written their proposals inline on Github issues. If requested I will be happy to do the extra work to copy the content over and format it correctly for markdown, but if that is not needed it would be great so I don't have to go to that effort.

Just let me know please.

@gopherbot gopherbot added this to the Proposal milestone Dec 30, 2018
@go101
Copy link

go101 commented Dec 30, 2018

you can put it here.

@mikeschinkel
Copy link
Author

Hi @go101, Thanks for the comment.

However, my proposal is not about generics. I also was not aware people are using the name contract in their generics proposals, although I do not think that invalidates what I am proposing.

@mvdan mvdan added LanguageChange v2 A language change or incompatible library change labels Dec 30, 2018
@bitfield
Copy link

This is interesting, but I think you'll definitely need a different name for what you're proposing, since 'contract' will mislead everyone to assume this is about generics.

I agree that large interfaces (like Dialect) are fragile, and don't really fit the spirit of Reader-style interfaces. I'm not sure that your proposal really fixes this, though. As you say, there's not much difference between a contract and an interface. Adding an optional keyword is likely to make things worse, rather than better, I think.

You say:

If Go adds a new contract type, and the contracts only supports aggregation of interfaces with not direct inclusion of method signatures then many small interfaces would actually be encouraged.

I certainly think the use of small interfaces, composed into larger ones where necessary, should be encouraged. But in general the design philosophy of Go seems to be to encourage desired behaviour by taking away alternatives, not by providing new ones.

@ianlancetaylor ianlancetaylor changed the title Proposal: Add contract type to Go proposal: Go 2: add contract type to Go Dec 30, 2018
@ianlancetaylor
Copy link
Contributor

If I'm reading the proposal correctly, contracts are essentially the same as interfaces. I personally don't find you rebuttal at the end to be convincing: just giving the type a different name doesn't make it usefully different.

The idea of an optional method is interesting, but you don't really explain how that works. I assume that it means that you can convert a type to the contract even if it doesn't implement the method, but what happens if the method is called?

@mikeschinkel mikeschinkel changed the title proposal: Go 2: add contract type to Go proposal: Go 2: Add new type designed to aggregate small interfaces Jan 1, 2019
@mikeschinkel
Copy link
Author

@bitfield Thanks for your comments

"This is interesting, but I think you'll definitely need a different name for what you're proposing, since 'contract' will mislead everyone to assume this is about generics."

When I saw that I used the same name as has been proposed for generics, I asked myself which concept makes better use of the name contract, and I honestly and objectively think that my proposal is more inline with how programmers have been speaking of "contracts" for the past decade or more.

Further I would argue, and if I have plans to I will argue this in a blog post (I have a large critical finish a project first), that what the Go team is calling a contract would much better be called a constraint, which I think makes their concept much easier to reason about.

"I agree that large interfaces (like Dialect) are fragile, and don't really fit the spirit of Reader-style interfaces. I'm not sure that your proposal really fixes this, though. As you say, there's not much difference between a contract and an interface."

The differences are:

1.) A contract would allow a developer to convey difference in intent from an interface,
2.) IDEs like GoLand could then add code inspections to flag multi-method interfaces, and
3.) go vet could be enhanced with a flag to report multi-method interfaces.

"Adding an optional keyword is likely to make things worse, rather than better, I think."

Can you elaborate on how you think optional would make things worse? Remember, I proposed is so that backwards compatible additions could be made to an interface and not break code code in the wild that uses prior versions of the interface.

And with your elaboration, maybe a proposal for an alternate that could address how to allow an interface to evolve in a backward compatible manner without breaking code in the wild?

"But in general the design philosophy of Go seems to be to encourage desired behaviour by taking away alternatives, not by providing new ones."

That I am aware of, and actually support in general. Except I would not call this an alternative; in Go2 this could actually be the only option for multi-method interfaces, if the Go team so chose to do so.

Also I would ask the rhetorical question, is the design philosophy motivated by a desire to keep things from being added? Or is it motivated to keep the language easy to reason about where developer's intentions are able to be very clearly indicated in code? If the former, then ok.

But if the latter I would argue adding contracts fits more in line with the Go philosophy vs. having the ambiguity of use-cases that interface{}s support. IMO anyway, the Go team clearly may differ.

@mikeschinkel
Copy link
Author

mikeschinkel commented Jan 1, 2019

@ianlancetaylor

Thank you so much for taking the time to comment.

"I'm reading the proposal correctly, contracts are essentially the same as interfaces."

As I envision them, contracts would be, by definition, be an aggregation of interfaces. So if Foo is a contract then func Bar(f Foo) would not see any difference between a contact or an interface.

OTOH, with contacts the developer who creates interfaces would know in Go2 interfaces should one have only method because for multiple methods you would need a contract.

The two really do address different use-cases. With contracts, these two use-cases could become clear to the reader of code, and contracts by nature could encourage people to use greater care when they define an interface because it could in fact be reused serendipitously.

Further, a struct could include a contract and by doing would have the compiler check to verify that the struct indeed included all the required methods. (As an aside, currently you cannot tell until runtime if you have implemented an interface correctly -- especially because an interface with a Foo receivers and *Foo receivers are viewed as different methods. It would be nice to match the compiler indicate that a contract is not being fulfilled, and why.)

"I personally don't find you rebuttal at the end to be convincing: just giving the type a different name doesn't make it usefully different."

Hopefully you find my above explanation more convincing? :-)

"The idea of an optional method is interesting, but you don't really explain how that works. I assume that it means that you can convert a type to the contract even if it doesn't implement the method, but what happens if the method is called?"

The same thing that happens when you assert a type on an interface; it would panic.

For optional methods the caller would be responsible in advance to verify that the method exists before calling it. And being marked as optional, the developer would be able to see that it is their responsibility, and I don't know enough about compilers to know for sure but I would assume that compiler could verify if it had first been checked before being called.

Another option could be that all optional methods could return the zero type if they do not exist but that they could be called like this for the developer to know if they existed or not:

x:= NewX()
result,ok := x.MyOptionalFunc()
if !ok {
    result= "default value"
}

@mikeschinkel mikeschinkel reopened this Jan 1, 2019
@mikeschinkel
Copy link
Author

@ianlancetaylor

Also, regardless of your decision on this proposal may you please allow me I suggest you consider renaming your proposed feature for generics to "constraints" instead of contracts?

I think what you have proposed feels more like a constraint than a contract, or at least it is easier to reason about as a constraint than as a contract, at least to me.

There is also the fact people have been referring to interfaces as contracts for decades, so I think that calling them contracts might cause confusion for new people coming to go. #JMTCW

@ianlancetaylor
Copy link
Contributor

In the generics design draft we chose to use the word contract because the contract defines the relationship between two different parts of the code: the function with type parameters, and the type arguments used to instantiated that function. Also, arguably, a contract is a collection of constraints, rather than a single constraint. I agree that the word "contract" is also used in different ways in other parts of the programming world. I think that's OK.

As far as this proposal goes, it's OK when a single concept serves multiple purposes. Personally I don't think it aids comprehensibility to give the multiple purposes different names. We already get plenty of discussion about the difference between new(T) and &T{}.

@ianlancetaylor
Copy link
Contributor

Adding a new concept to the language carries a heavy cost. The benefit here does not seem nearly enough for the cost. The distinction between interfaces and contracts can be expressed almost as well by writing a comment.

@mikeschinkel
Copy link
Author

mikeschinkel commented Jan 16, 2019

@ianlancetaylor Understood, and I appreciate the consideration you gave this proposal.

However, there were two (2) aspect of this it seems that your consideration did not address when you closed this ticket which were actually the motivation for requesting contracts:

  1. Allow developers to optional specify explicitly that a struct implements an interface. I an not asking for all interface use to require an explicit implements but instead only for those cases that I was envisioning contract would be used for. The main benefit here is the compiler could validate that all the required methods are implemented correctly and if not fail to compile. (Note: I am constantly implementing interfaces that are wrong such as by pointer receiver vs. non-pointer receiver and they just don't work even though I assume they are working. Non-matching interfaces are a hidden "gotcha" in Go, similar to how shadowed variables are a gotcha. It would be great if IDE's like GoLand could also tell me exactly what I need to implement and even let developers choose to have the IDE automatically stub in the required methods.)

  2. Allow developers to specify that methods are optional. This would be beneficial for (at least) my use-cases in several different ways, especially for methods that do not return values and this could be treated as no-ops when called if they were not implemented for the current instance. If they do return values, then calling them with the value,ok := instance.method() syntax could be the way to test if they exist or not.

BTW, #1 without #2 would not really be good because it would not allow implemented interfaces to evolve in backward compatible ways.

(Should I create a new ticket for these instead of discussing on this closed ticket?)

@randall77
Copy link
Contributor

randall77 commented Jan 16, 2019

The canonical way to assert that a type *T implements an interface I is to do:

var _ I = (*T)(nil)

That should solve #1 for you without requiring a language change.

@randall77
Copy link
Contributor

Also, #2 can be done in the current language with interfaces. If the optional method is Bar, do:

type I interface {
    Foo()
}
type I2 interface {
    I
    Bar()
}

Then at a use point, you can do

func use(i I) {
    i.Foo()
    if j, ok := i.(I2); ok {
        j.Bar()
    }
}

(I2 doesn't need to extend I even.)

@mikeschinkel
Copy link
Author

mikeschinkel commented Jan 16, 2019

Hi @randall77 - Thanks so much for the super quick response.

The canonical way to assert that a type *T implements an interface I is to do:

var _ I = (*T)(nil)

That should solve #1 for you without requiring a language change.

Thank you for that; doing that had not occurred to me.

However it only addresses the issue on selected use-cases but not all, see below for more.

Also, it is not as discoverable as an explicit keyword, as evidenced by the fact I've been lamenting the need for this for months, I've googled for it to no avail, and had to make a feature request to discover it.

If we had the following it would be more discoverable for a programmer new to Go:

type S struct implements I {}

Also, #2 can be done in the current language with interfaces.

Yes, thank you. I have been using this technique as a fall back for what I was asking for.

But it does not actually address the use-case for which I proposed a new type designed to aggregate small interfaces. And that use-case could be addressed by extending interfaces (I still think a new type would be more elegant as a new concept, but accept that that is not in the cards.)

Your example, repeated here, addresses the use-case for a single-method interface being optional,
but not a multi-method interface with optional methods:

type I interface {
    Foo()
}
type I2 interface {
    I
    Bar()
}
func use(i I) {
    i.Foo()
    if j, ok := i.(I2); ok {
        j.Bar()
    }
}

Here, a more fleshed-out example showing a multi-method interface which is the use case
that your offered technique does not resolve in the case some methods are required and others are optional:

type I interface {
	Foo()
	Bar()
	Baz()
}

func use(i I) {     
	i.Foo()
	// do stuff
	i.Bar()
	// do more stuff
	i.Baz()
}

type S struct {}  // S does not implement Baz(), which we want to be optional
func (s S) Foo() {}
func (s S) Bar() {}  

func main() {
	use(S{})  // won't compile because S does not implement Baz()
}

Here is what I have been doing to workaround the lack of optional methods in interfaces in Go:

type I interface {}

type Fooer interface{
	Foo()
}
type Barer interface{
	Bar()
}
type Bazer interface{
	Baz()
}

func use(i I) {

	if j, ok := i.(Fooer); ok {
		j.Foo()
	}
	// do stuff
	if j, ok := i.(Barer); ok {
		j.Bar()
	}
	// do more stuff
	if j, ok := i.(Bazer); ok {
		j.Baz()
	}
}

A key problem with this workaround is nowhere in the code can we declare to the compiler or an IDE, including with your idiomatic suggestion, that any use of interface I must implement Foo() and Bar() but maybe Baz(). The associations are purely implicit.

Yes they could be commented but not in a manner that an IDE could recognize and flag or the compiler could validate.

Here is what I propose instead:

type I interface {}
	Foo()
	Bar()
	optional Baz()  // or some other syntax to denote optional
}

func use(i I) {
	i.Foo()
	// do stuff
	i.Bar()
	// do more stuff
	i.Baz()
}

Also, the above is easier to read and three (3) lines of calling code instead of nine (9).

Plus, as a side bonus, 15 fewer presses of the shift key meaning my wrist would be thankful
as it might mean fewer periodic carpel tunnel flareups!

I hope this clarifies?


As an aside, here is why I still lament the lack of interest in a new type designed to aggregate small interfaces.

Let us assume we have a different type than interface for aggregating small interfaces. I will call it enforcer here (since @ianlancetaylor prefers his use of the term contract over mine) as it would "enforce" that all multi-method use-cases are comprised of individual single method interfaces vs. allowing proliferation of multi-method interfaces without exposing each method using a single-method interface:

type E enforcer {
    Foo()
    Bar()
    optional Baz()
}

type Fooer interface{
	Foo()
}
type Barer interface{
	Bar()
}
type Bazer interface{
	Baz()
}

#fwiw

@randall77
Copy link
Contributor

Also, it is not as discoverable as an explicit keyword, as evidenced by the fact I've been lamenting the need for this for months, I've googled for it to no avail, and had to make a feature request to discover it.

Go has a high bar for adding new features to the language. That bar is especially high when there's already a way within the language to do what a new feature would do. Even more so when the way to do it is a one-liner.

Your point about discoverability is a good one, though. We should collect nuggets of knowledge like this somewhere. Maybe in the wiki?

A key problem with this workaround is nowhere in the code can we declare to the compiler or an IDE, including with your idiomatic suggestion, that any use of interface I must implement Foo() and Bar() but maybe Baz(). The associations are purely implicit.

I'm confused by this claim. If Foo and Bar are required, but Baz is optional, then do:

type I interface {
	Foo()
	Bar()
}
type Bazer interface{
	Baz()
}

func use(i I) {
	j.Foo()
	j.Bar()
	// do more stuff
	if j, ok := i.(Bazer); ok {
		j.Baz()
	}
}

Or is that not what you mean?

@mikeschinkel
Copy link
Author

mikeschinkel commented Jan 16, 2019

Go has a high bar for adding new features to the language. That bar is especially high when there's already a way within the language to do what a new feature would do. Even more so when the way to do it is a one-liner.

I can appreciate that, and is one of the reason I generally really like Go as a language.

Which is why anything I might propose I first would want to see it simplify the code a developer would need to write and that other developers would need to read *and make written code more clear to a reader, and I think my proposal reaches that bar, no?

Your point about discoverability is a good one, though. We should collect nuggets of knowledge like this somewhere. Maybe in the wiki?

Yes.

I'm confused by this claim. If Foo and Bar are required, but Baz is optional, then do:

Yes, that is possible, but it does not explicitly define an association between I and Bazer.

Let's discuss this with more concrete class name; using abstract names like I, Foo, Bar and Baz makes it much harder to appreciate valid use cases IMO.

Let's assume I want to implement a "connectors" (my word) for an Email Service Provider (ESP.) I want to provide connectors for SendMail, Amazon SES, and Mailjet but leave the door open to add connectors for Postmark or SendInBlue in the future (or to let someone else write those.)

Further, let's assume that while these services are similar there are differences in these services. Let's assume some offer delivery receipts and others do not. Some allow sending in batch, some only allow sending one at a time, and potentially a lot more differences.

What I would like to do is define an interface named EspConnector or something similar with all the potential connector methods to implement a valid connector for a specific email service provider, including optional ones. Further I would like to be able to specify that anyone who implements those must implement certain methods but that other methods are optional and that they may implement those but are not required to.

Given your proposed solution I would need to define an EspConnector and then an EspDeliveryReciepter interface and another EspBatchSender interface, and maybe many others.
And while that is certainly doable it is does not define explicitly that EspDeliveryReciepter and EspBatchSender requires EspConnector to be implemented thus no way for the compiler to enforce the requirement nor for an IDE to police incomplete code.

It is also more complexity for my implementing code and for usage code, and as people already complain about how required error handling makes Go code more complex than it needs to be it would seem to me that adding optional to interface methods would be an easy win that would simplify both code implementation and usage of code that uses the interfaces. (Plus it would also reduce the complexity of my documentation explaining which interfaces people need to implement for my connector.) No?

@ianlancetaylor
Copy link
Contributor

Which is why anything I might propose I first would want to see it simplify the code a developer would need to write and that other developers would need to read *and make written code more clear to a reader, and I think my proposal reaches that bar, no?

All changes make things better in some way. Nobody proposes useless changes.

But all changes also have costs. Adding anything to the language carries a heavy cost: every user of the language has to learn the new concept.

When considering any language change, we must consider not only the benefits, we must also consider the costs. Does the change bring enough new value to justify the cost of making everybody learn about it?

See also https://blog.golang.org/go2-here-we-come for more discussion about when to make a change to the language.

@mikeschinkel
Copy link
Author

mikeschinkel commented Jan 16, 2019

@ianlancetaylor Thanks on your link. Based on your three criteria:

A proposal must at the very least:

  1. address an important issue for many people,
  2. have minimal impact on everybody else, and
  3. come with a clear and well-understood solution.

It would seem optional methods would definitely check off #2 and #3 so then the only question would be are there enough people who would appreciate it that they could be described as "many?"

With your blessing I would like to add a new proposal for optional methods to see if there are indeed other Go developers for whom it would address their needs enough that it could be indentified as "many?"

@randall77
Copy link
Contributor

And while that is certainly doable it is does not define explicitly that EspDeliveryReciepter and EspBatchSender requires EspConnector to be implemented thus no way for the compiler to enforce the requirement nor for an IDE to police incomplete code.

That's exactly what interface embedding is for. Embed EspConnector in EspDeliveryReciepter and EspBatchSender, and then no type can be assigned to either of the latter two without implementing all of EspConnector also.

@mikeschinkel
Copy link
Author

That's exactly what interface embedding is for.

Ah, okay. Then I guess the remaining issue is the difference between having to write all this code:

if j, ok := i.(Fooer); ok {
    j.Foo()
}
if j, ok := i.(Barer); ok {
    j.Bar()
}
if j, ok := i.(Bazer); ok {
    j.Baz()
}	

vs. just being able to write this:

i.Foo()
i.Bar()
i.Baz()

@golang golang locked and limited conversation to collaborators Jan 16, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
FrozenDueToAge LanguageChange Proposal v2 A language change or incompatible library change
Projects
None yet
Development

No branches or pull requests

7 participants