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: Allow function to satisfy interface while returning interface superset #37129

Closed
phemmer opened this issue Feb 7, 2020 · 4 comments
Labels
Milestone

Comments

@phemmer
Copy link

phemmer commented Feb 7, 2020

Current behavior

Currently for a function to satisfy an interface, the signature of the function has to match the interface signature exactly.
In the case where the interface signature says the function should return an interface, the function can only return that interface, and not another interface which is a superset.

Meaning if you have the interfaces:

type ReaderOpener interface {
  Open() io.Reader
}
type ReadSeekerOpener interface {
  Open() io.ReadSeeker
}

Then the following type would satisfy ReadSeekerOpener, but not ReaderOpener.

type MyStruct struct {}
func (ms MyStruct) Open() io.ReadSeeker {}

Attempting to use MyStruct to satisfy ReaderOpener results in:

cannot use MyStruct literal (type MyStruct) as type ReaderOpener in assignment:
	MyStruct does not implement ReaderOpener (wrong type for Open method)
		have Open() io.ReadSeeker
		want Open() io.Reader

Proposal

The proposal is to allow functions returning an interface to satisfy the interface if the function returns an interface which is a superset of the one being satisfied.

Meaning that in the above code, MyStruct would satisfy both ReadSeekerOpener and ReaderOpener.

Now there is one particular about how "superset" is defined. It could either be 2 completely different interfaces with no inheritance, or one interface could be a composition containing the other.
Meaning:

type Reader interface {
  Read([]byte) (int, error)
}
type ReadSeeker {
  Read([]byte) (int, error)
  Seek(int64, int) (int64, error)
}

vs.

type Reader interface {
  Read([]byte) (int, error)
}
type ReadSeeker {
  Reader
  Seek(int64, int) (int64, error)
}

The key difference being that the latter ReadSeeker interface embeds the interface required in the ReaderOpener Open() signature.

While I think it might be nice if both would satisfy the proposed rule, if it matters, I think it would be reasonable to allow the latter to satisfy the proposal, but not the former.

Additional considerations

Explicit types

There's also the related case of an explicit type:

type ReaderOpener func() io.Reader

and whether a function which returns io.ReadSeeker should be assignable to a variable of this type.

I don't know that this needs to be addressed in this proposal, but I mention it as it seems related.

Function argument subsets

In theory the return superset logic should be able to be applied to function arguments, and whether interfaces can be subsets. Meaning whether a function with the signature func(io.Reader) should satisfy interface { func(io.ReadSeeker) }. In theory since the function only needs io.Reader, if it's given io.ReadSeeker, it should be able to operate normally.
So the consideration is that if return values are relaxed to allow interface supersets. Should function arguments be relaxed to allow interface subsets?

@gopherbot gopherbot added this to the Proposal milestone Feb 7, 2020
@ianlancetaylor ianlancetaylor added v2 A language change or incompatible library change LanguageChange labels Feb 7, 2020
@ianlancetaylor
Copy link
Contributor

This seems related to this entry in the FAQ: https://golang.org/doc/faq#covariant_types.

@phemmer
Copy link
Author

phemmer commented Feb 7, 2020

Indeed it does. I was thinking that going from one interface to another might be a little different from that case of going from a concrete type to an interface, especially if the superset interface embeds the one in the signature. But maybe they're the same.

The other thought behind why I was thinking the proposal makes sense is that go allows assignment to variables of an interface type as long as that interface is satisfied (regardless of whether it's another interface, or a concrete type).

open := func() io.ReadSeeker { ... }
var reader io.Reader = open()

So if reader was defined as an io.Reader, but we gave it a io.ReadSeeker, and this works, I'm not sure I see much difference.
Though this does seem pretty spot on to one part of that FAQ entry about

trying to express a type hierarchy through interfaces

Which if that's something just rejected on principal, I can understand that (go's opinionated nature is one of the best things about it). I think the proposal would make some things easier, but it's not like it can't be worked around.

@ianlancetaylor
Copy link
Contributor

The language doesn't support covariant returns in interface results, and that is an intentional decision as discussed in the FAQ. Therefore, this proposal is a likely decline. Leaving open for four weeks for final comments.

@ianlancetaylor
Copy link
Contributor

No further comments. Closing.

@golang golang locked and limited conversation to collaborators Mar 24, 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

3 participants