-
Notifications
You must be signed in to change notification settings - Fork 105
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
Type aliases for generics #451
Comments
This sounds like an excellent idea, IMO! Another pain point in the similar vein is the need for |
In theory, I like it. However, there is an issue with name conflicts. What happens if you have: // a/a.proto
package a;
message One {} // b/b.proto
package b;
import "a/a.proto";
message One {}
service OneService {
rpc AOne(a.One) returns (a.One);
rpc BOne(One) returns (One);
} You'll have to derive a scheme to deal with this. The most obvious is to prefix each aliased name with RPC name, but even this doesn't work, consider: // b/b.proto
package b;
import "a/a.proto";
message One {}
service OneService {
rpc One(One) returns (One);
}
service TwoService {
rpc One(a.One) returns (a.One);
} So then, you have to prefix with either the package name, or the service AND RPC name. The results are not pretty: // packageName
type BufConnectDemoV1SayRequest = connect_go.Request[v1.SayRequest]
type BufConnectDemoV1SayResponse = connect_go.Response[v1.SayResponse]
// service and RPC name
type ElizaService_Say_SayRequest = connect_go.Request[v1.SayRequest]
type ElizaService_Say_SayResponse = connect.go.Response[v1.SayResponse] And even this doesn't get you what you need. In our above example, // service and RPC name
type ElizaService_Say_SayRequestRequest = connect_go.Request[v1.SayRequest]
type ElizaService_Say_SayRequestResponse = connect_go.Response[v1.SayRequest]
type ElizaService_Say_SayResponseRequest = connect.go.Request[v1.SayResponse]
type ElizaService_Say_SayResponseResponse = connect.go.Response[v1.SayResponse] I'm not sure if there is a solution that doesn't result in ugly naming, but perhaps you both can come up with one. |
I think that either:
|
This seems reasonable to me. It handles the common case nicely, results in good names, and isn't overly complex. My main reservation with this proposal is that it prioritizes short function signatures over obviousness. If we alias |
What are people's thoughts on the following ... While developing a demo application with connect-go, just to try out some possible solutions to this issue, I manually added the following type aliases to the request and response types in the demo:
That allowed for usage where the package name would still make it clear which type you were referring to even if multiple Go packages defined
Though not as short as some other solutions, it did at lease remove the stuttering, eg: That seems nice since it is suggested that developers always add |
@marekbuild That's nice and avoids conflicts, but it generates code into the base types - which we don't want to do, because it's messy and incompatible with BSR remote packages. |
We've let this sit for quite a while. I think we can move forward with generating type aliases in the Connect packages. Regarding Peter's concern with name collisions, I think the best course of action is to not generate aliases for poorly-behaved request/response messages. (We can generate comments explaining why.) The idea is to reduce boilerplate and wordy type names where possible, and very long identifiers don't help. Regarding my earlier concern about people maybe forgetting to use constructors, I think we're okay - it's currently safe for people to skip the constructor and do Whenever we tackle this, we should be sure to also update the README, all the examples, the tests, the Connect docs, and the ancillary repositories (health, reflection, etc) to use the aliases. |
Tried an idea for generating types based on service and method name prefix, similar to how procedure, client and server interfaces are namespaced. Ping(context.Context, *connect_go.Request[v1.PingRequest]) (*connect_go.Response[v1.PingResponse], error)
Fail(context.Context, *connect_go.Request[v1.FailRequest]) (*connect_go.Response[v1.FailResponse], error)
Sum(context.Context, *connect_go.ClientStream[v1.SumRequest]) (*connect_go.Response[v1.SumResponse], error)
CountUp(context.Context, *connect_go.Request[v1.CountUpRequest], *connect_go.ServerStream[v1.CountUpResponse]) error
CumSum(context.Context, *connect_go.BidiStream[v1.CumSumRequest, v1.CumSumResponse]) error is now: Ping(context.Context, *PingServicePingRequest) (*PingServicePingResponse, error)
Fail(context.Context, *PingServiceFailRequest) (*PingServiceFailResponse, error)
Sum(context.Context, *PingServiceSumStream) (*PingServiceSumResponse, error)
CountUp(context.Context, *PingServiceCountUpRequest, *PingServiceCountUpStream) error
CumSum(context.Context, *PingServiceCumSumStream) error But once you try implement it the type signature gets pretty unwieldy again. Ping(context.Context, *pingv1connect.PingServicePingRequest) (*pingv1connect.PingServicePingResponse, error)
Fail(context.Context, *pingv1connect.PingServiceFailRequest) (*pingv1connect.PingServiceFailResponse, error)
Sum(context.Context, *pingv1connect.PingServiceSumStream) (*pingv1connect.PingServiceSumResponse, error)
CountUp(context.Context, *pingv1connect.PingServiceCountUpRequest, *pingv1connect.PingServiceCountUpStream) error
CumSum(context.Context, *pingv1connect.PingServiceCumSumStream) error |
Do we need the service name prefix? Just this should be fine: Ping(context.Context, *PingRequest) (*PingResponse, error)
Fail(context.Context, *FailRequest) (*FailResponse, error)
Sum(context.Context, *SumStream) (*SumResponse, error)
CountUp(context.Context, *CountUpRequest, *CountUpStream) error
CumSum(context.Context, *CumSumStream) error |
It needs the service to separate the method names: service FailService {
rpc Ping(FailRequest) returns (FailResponse) {}
} Would be overlapping with Ping(context.Context, *FailServicePingRequest) (*FailServicePingResponse, error) |
I assume that you meant But why does it matter that it will be overlapping, if they are in the same proto package, then |
@meling it's name is based on the service and RPC method, not the message type. So for the two type (
// PingService.Ping
PingServicePingRequest = connect_go.Request[v1.PingRequest]
PingServicePingResponse = connect_go.Response[v1.PingResponse]
// FailService.Ping
FailServicePingRequest = connect_go.Request[v1.FailRequest]
FailServicePingResponse = connect_go.Response[v1.FailResponse]
) I think this will avoid issues with types imported from other package potentially conflicting with local types. If you have an RPC like: service LibraryService {
rpc DeleteBook(DeleteBookRequest) returns (google.protobuf.Empty)
} The types would be: type (
LibraryServiceDeleteBookRequest = connect_go.Request[library.DeleteBookRequest]
LibraryServiceDeleteBookResponse = connect_go.Response[empty.Empty]
) |
I think this can be solved with shorter names by default and use longer names for conflict scenarios. @emcfarlane Building on your example, I suggest instead to generate this by default: type (
// PingService.Ping
PingRequest = connect_go.Request[v1.PingRequest]
// FailService.Ping
FailRequest = connect_go.Request[v1.FailRequest]
) But of course, you can have another package type (
// PingService.Ping
PingRequest = connect_go.Request[v1.PingRequest]
// FailService.Ping
PingRequest = connect_go.Request[x2.PingRequest]
) However, I think such conflict scenarios are quite rare. IIRC, I think there are some code generation strategies in type (
// PingService.Ping
PingServicePingRequest = connect_go.Request[v1.PingRequest]
// FailService.Ping
FailServicePingRequest = connect_go.Request[x2.PingRequest]
) Moreover, if |
Would be great to have something shorter! I don't think we can use on conflicts, as introducing a message that causes a conflict will then break all the previous implementations. It has to avoid conflicts without knowing about all other types. We could prepend the package only on external packages, so: type (
PingRequest = connect_go.Request[v1.PingRequest]
BufConnectDemoV2PingRequest = connect_go.Request[x2.PingRequest]
) Which makes the simple case, simple. But still causes conflicts if you name your non external type type CumSumRequestCumSumResponse = connect_go.BidiStream[v1.CumSumRequest, v1.CumSumResponse] This could conflict with package names like bufdev showed above, so would need a separator between the types. Also would have to have a suffix on each type to avoid I think using message types is too tricky to cover all edge cases. But the more succinct versions would be ideal, so be great to explore more. |
I've opened #560 - it's a little different from @emcfarlane's approach. In general, it's more conservative and only generates aliases for the well-behaved subset of messages where we can safely use short names. I also skipped generating aliases for the stream types because they get very wordy. If there's appetite for them, we can add them later. From a user's perspective, the Ping service goes from Ping(context.Context, *connect.Request[pingv1.PingRequest]) (*connect.Response[pingv1.PingResponse], error)
Fail(context.Context, *connect.Request[pingv1.FailRequest]) (*connect.Response[pingv1.FailResponse], error)
Sum(context.Context, *connect.ClientStream[pingv1.SumRequest]) (*connect.Response[pingv1.SumResponse], error)
CountUp(context.Context, *connect.Request[pingv1.CountUpRequest], *connect.ServerStream[pingv1.CountUpResponse]) error
CumSum(context.Context, *connect.BidiStream[pingv1.CumSumRequest, pingv1.CumSumResponse]) error to Ping(context.Context, *pingv1connect.PingRequest) (*pingv1connect.PingResponse, error)
Fail(context.Context, *pingv1connect.FailRequest) (*pingv1connect.FailResponse, error)
Sum(context.Context, *connect.ClientStream[pingv1.SumRequest]) (*pingv1connect.SumResponse, error)
CountUp(context.Context, *pingv1connect.CountUpRequest, *connect.ServerStream[pingv1.CountUpResponse]) error
CumSum(context.Context, *connect.BidiStream[pingv1.CumSumRequest, pingv1.CumSumResponse]) error |
I'm in favor of #560. However, how bad is the streaming options considered; it wasn't clear to me. Could you write out the examples? Wouldn't this work? Sum(context.Context, *connect.ClientStream[pingv1.SumRequest]) (*pingv1connect.SumResponse, error)
CountUp(context.Context, *pingv1connect.CountUpRequest, *connect.ServerStream[pingv1.CountUpResponse]) error
CumSum(context.Context, *connect.BidiStream[pingv1.CumSumRequest, pingv1.CumSumResponse]) error to Sum(context.Context, *pingv1connect.SumStream) (*pingv1connect.SumResponse, error)
CountUp(context.Context, *pingv1connect.CountUpRequest, *pingv1connect.CountUpResponseStream) error
CumSum(context.Context, *pingv1connect.CumSumBidiStream) error or Sum(context.Context, *pingv1connect.SumClientStream) (*pingv1connect.SumResponse, error)
CountUp(context.Context, *pingv1connect.CountUpRequest, *pingv1connect.CountUpServerStream) error
CumSum(context.Context, *pingv1connect.CumSumBidiStream) error If client and server streams must be distinguishable. |
@meling Generating aliases for the stream types isn't infeasible, it's just more complex - there are more naming decisions and potential conflicts to worry about, so there's more to debate. This is especially true if we proceed with the approach in #560, where the aliases are derived from the message names rather than the service + method names. I wasn't confident that we'd reach agreement even for the simpler unary aliases, so I wanted to push any discussion of streaming to a future PR :) |
#560 doesn't have majority approval either 🤣 In the discussion, @mattrobenolt made a good suggestion to consider using custom options: #560 (comment) Pursuing that approach is probably the next step here. |
connect.Request
, connect.Response
and stream types
Whenever I'm writing a new connect service it always seems frustrating that it seems like I've got a lot of noise in the function parameter:
Even with the simplest unary functions the verbosity forces for multi line function signatures which can be an eye sore:
To solve this, a
SayRequest
could be generated withinelizav1connect
such that no generic syntax shows up in the function signature:This would allow for a simpler function signature to implement:
benefits:
Making library decisions around IDEs is probably not a great justification, but I do find myself often getting confused by the numerous
*connect.Request
,*connect.Response
and other types.A similar thing could be done with streaming endpoints too:
The text was updated successfully, but these errors were encountered: