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: interface adapter types #26757

Closed
ianlancetaylor opened this issue Aug 2, 2018 · 12 comments
Closed

proposal: Go 2: interface adapter types #26757

ianlancetaylor opened this issue Aug 2, 2018 · 12 comments
Labels
FrozenDueToAge LanguageChange Proposal v2 A language change or incompatible library change
Milestone

Comments

@ianlancetaylor
Copy link
Contributor

In large Go programs written by many different people, useful interfaces may develop after considerable code has been written. These interfaces can capture useful common features of types, but the methods of those types may not have the same signatures. For a simplified example, we may have code like

type T1 struct { ... }
func (t1 *T1) Val() string { ... }

type T2 struct { ... }
func (t2 *T2) Fetch() T1

type U1 struct { ... }
func (u1 *U1) Val() string { ... }

type U2 struct { ... }
func (u2 *U2) Fetch() U1

With this kind of example we can, after the fact, define an interface that can be used for both T1 and U1 (interface { Val() string }). But there is no interface we can write for both T2 and U2, because the Fetch methods return different types. The methods are sufficiently similar that we can do it with some boilerplate code:

type I1 interface { Val() string }
type I2 interface { Fetch() I1 }
type T2Wrapper struct { T2 }
func (t2w *T2Wrapper) Fetch() I1 { return t2w.T2.Fetch() }
type U2Wrapper struct { U2 }
func (u2w *U2Wrapper) Fetch() I1 { return u2w.U2.Fetch() }

Now T2Wrapper and U2Wrapper implement I2, thanks to the implicit conversion of the respective result parameter to I1. But you do have to write the T2Wrapper and U2Wrapper types.

A similar case arises when a package wants to represent errors using a specific type that is more structured than the predefined error type. It's natural to write methods that return that structured error type, but then the types implementing those methods do not satisfy standard interfaces such as io.Reader. If the structured error type is itself an interface type, then boilerplate wrappers like the above would permit the interface to be satisfied (the error type must be an interface type to avoid the confusion in which a nil non-interface type becomes a non-nil error value).

To make these sorts of cases simpler to write in Go with less boilerplate, I propose a new kind of type: an adapter type. An adapter type links a non-interface type to an interface type by returning a new type that defines the methods expected by the interface by calling the methods defined by the non-interface type.

type I interface { ... }
type T ...
type A adapter[T]I

In general the type adapter[T]I is a type that acts exactly like struct { T }, except that it has the same methods as I where each method is implemented by calling the method of the same name on T. Every argument type of a method of I must be convertible to the type of the corresponding argument of the corresponding method of T. Every result type of that method must be convertible to the corresponding result of the method of I. In general, given a method M(A1, A2) (R1, R2) on I and a method M(B1, B2) (S1, S2) on T, adapter[T]I has a method

// This method would not be written in the program, it is implicitly generated.
func (a *adapter[T]I) M(a1 A1, a2 A2) (R1, R2) {
    r1, r2 := a.T.M(B1(a1), B2(a2)
    return R1(r1), R2(r2)
}

If T does not have a method M, or if the number of argument or result types differ, or if the explicit conversions to the corresponding types are not valid, then the adapter type is erroneous and compilation fails.

I'm not persuaded that this is a good idea, but I wanted to put it out there as a way to address an issue that arises in large Go programs. I note that this could be implemented entirely using code generation, and that might be the most appropriate choice.

@ianlancetaylor ianlancetaylor added LanguageChange v2 A language change or incompatible library change Proposal labels Aug 2, 2018
@ianlancetaylor ianlancetaylor added this to the Proposal milestone Aug 2, 2018
@jimmyfrasche
Copy link
Member

I'm unclear on how you would use adapter[T]I. A value of T would be assignable to it and it would satisfy I? What happens when you call a method on an adapter that is not wrapping a T? Can you/how do you get the T back out of the adapter?

@ianlancetaylor
Copy link
Contributor Author

Oh yeah, I did leave that part out. Given a value t of type T, you can write (adapter[T]I)(t) to get a value of type adapter[T]I, and of course that value implements I and is therefore assignable to I. You can go back with an explicit conversion to T, which given a value i of type I looks like T(i.(adapter[T]I)).

@smasher164
Copy link
Member

This does seem to address the problem of wrapping known methods. How would type assertions work on a value instantiated from an adapter type? Can it uncover methods that are implemented by the underlying concrete type?

@mvdan
Copy link
Member

mvdan commented Aug 2, 2018

If some form of generics were added to Go2, wouldn't they likely solve this issue? Or rather, perhaps this problem should be kept in mind for a future generics design or proposal.

@yiyus
Copy link

yiyus commented Aug 2, 2018

I admit I have not thought too much about it but I think that, if interface literal types make it into Go2 (issue #25860), this may be done without changing the language.

If we had interface literals, I would expect to be able to create interface values with some reflect function that takes a list of functions. We can already get the method set for a type, again using the reflect package. An hypothetical reflect.Adapter function would take a value of some type and an interface type, would create the wrappers, and would call the corresponding function to generate the interface value.

I am not totally sure this would be possible. It also has the drawback that you lose the information about the underlying type, although maybe an additional method may be used for that.

@Merovius
Copy link
Contributor

Merovius commented Aug 2, 2018

@yiyus That seems not connected to having interface literals though. It just requires being able to add methods or interface values in reflect and there is already an issue about that: #16522

IMO, I would much rather have real co-/contravariance for func and interface. I recognize the implementation difficulty of that though (mainly in regards to reflection and type assertions). This proposal seems still too unergonomic to be super useful, to be honest.

@ianlancetaylor
Copy link
Contributor Author

@mvdan I don't see how to do this using generics, unless by generics you mean some kind of generalized compile-time code-generation mechanism. The problem is that you need to construct an adapter type A that has all the methods of some interface type I, but without having to actually write those methods for A. If you have to write the methods, you haven't gained anything over just writing out the new type yourself. And I don't see how generics helps you discover the methods.

@mvdan
Copy link
Member

mvdan commented Aug 3, 2018

@ianlancetaylor you're right - I hadn't thought about this well enough. It still seems to me like this is worth considering together with generics for Go2, though. Particularly since we don't yet know how generics would fit into the language.

@jimmyfrasche
Copy link
Member

A related problem† is a type that would satisfy an interface if one or more of its operations were named differently.

† exacerbated by introducing generics that can use operators without either operator overloading or type classes (not that I think either of those are a good match for Go)

Say there is a generic Ordered interface with methods

Equal(T) bool
Less(T) bool

A value of time.Time almost satisfies Ordered(time.Time). It has an Equal method and it has a Before method which is Less in all but name. If the adapter concept were extended with renaming an adapter could be written something like

type Otime adapter[time.Time]Ordered(time.Time) {
  Before => Less
  // Equal is implicit as it already matches
}

Likewise, an int has all the correct operations but none of the correct names. If operators were allowed in the renaming it could look something like

type Oint adapter[int]Ordered(int) {
  == => Equal
  < => Less
}

(With the reverse direction, Equal => ==, explicitly disallowed).

Of course, an explicit wrapper type would provide the same function without a great deal of effort.

@bcmills
Copy link
Contributor

bcmills commented Sep 14, 2018

I don't see how to do this using generics

You can solve it on the caller side using the contracts in the current draft, but that doesn't help with the common interface per se: it moves the abstraction from run-time to compile-time.

interface StringValued {
	Val() string
}

contract StringValueFetcher(x T) {
	var _ StringValued = x.Fetch()
}

func FetchValue(type Fetcher StringValueFetcher)(x Fetcher) string {
	return x.Fetch().Val()
}

@bcmills
Copy link
Contributor

bcmills commented Sep 14, 2018

The problem is that you need to construct an adapter type A that has all the methods of some interface type I, but without having to actually write those methods for A.

That seems very similar to a more general problem: defining wrappers that pass through additional methods (examples: #16474, #21904, #27617; experience report here).

Since this problem and that one seem closely related, it would be nice if the solution could generalize to both. (It is not obvious to me whether this proposal does.)

@ianlancetaylor
Copy link
Contributor Author

This is an interesting problem space but this mechanism is too complicated. We aren't going to accept this proposal.

@golang golang locked and limited conversation to collaborators Oct 2, 2019
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

8 participants