Open dnadoba opened 3 years ago
I think this represents an interesting idea.
It's not got the full fidelity we want: ideally we'd support compile-time correctness of pipeline modification as well. However, that's necessarily hard, and this gets us a lot closer. @weissi what do you think of this idea?
I'm totally open to adding such functionality if users think it helps them. The pipeline itself cannot be compile-time type safe because we support mutation but we can offer helpers that make it easier to build a type-safe pipeline. Those helpers would obviously not cover 100% of the use cases but would maybe help for a good chunk of the standard use cases.
I'd think we could add an API that looks something like.
channel.pipeline.makePipelineBuilder()
.add(HandlerFoo())
.add(HandlerBar())
.add(HandlerBuz())
.build()
or so where the makePipelineBuilder
could be similar to what's proposed here and the .build()
would then just call into channel.pipeline.addHandlers(...)
and actually perform the job.
I think this could be a nice middle ground where many NIO programmers can use the type-safe API without losing any performance (or fidelity) at runtime.
When it comes to "Swifty" APIs we should ask ourselves whether a ResultBuilder is an appropriate way to express this API. It's potentially hard for us to use one, but still worth investigating.
When it comes to "Swifty" APIs we should ask ourselves whether a ResultBuilder is an appropriate way to express this API. It's potentially hard for us to use one, but still worth investigating.
We should definitely investigate. I don't think it'll work well for us (5.0 support / mix of types) but it's 100% worth looking into this.
@dnadoba Some other thoughts that I should probably leave here.
Handler1() <==> Handler2() <==> Handler3() <==> Handler4() ...
which would only compile if the types are compatibleChannelHandler
s have 2 main data inputs (InboundIn
/OutboundIn
) and 2 main data outputs (InboundOut
/OutboundOut
), #1139 requires them to all match.
Ie. it allows you to pair up two handlers if L.InboundOut == R.InboundIn, R.OutboundOut == L.OutboundIn
. A whole pipeline is built by repeatedly pairing up (and to make it look nicer, it provides the <==>
operator)The biggest reason I didn't further pursue #1139 is that many many handlers don't actually care what the inbound/outbound data types are because they just let them through. Ie. they don't implement both write
and channelRead
so the default implementation will just hand that through. That however makes the type-matching hard. In your proposal, this is somewhat less of a concern because you could overload the .add(...)
functions so for a ChannelInboundHandler
you could only check that the Inbound
types match. That would work for more handlers, however there are still problems:
ChannelInboundHandler
s do actually look at the data (I guess you could try to special-case Any
?)ChannelInboundHandler
s are also allowed to call write
, ie. they can inject writes into the pipelineself.unwrap(In|Out)boundIn
and self.wrap(In|Out)boundOut
won't go away (which is a thing that #1139 does).related/dupe of #39
Hi everyone,
I have played around with a type-safe Pipeline Builder by using phantom-types. It makes use of the associated types from
ChannelInboundHandler
andChannelOutboundHandler
to guarantee that handlers have compatible input and output types.Pipeline Builder is a struct with two generic parameters:
InboundIn
andOutboundOut
:A Pipeline is started with some start InboundIn and OutboundOut types, e.g.:
After that, we can add channel handlers with the add method:
pipeline
does now have the typePipelineBuilder<Frame, ByteBuffer>
. We can also add an encoder:and now
pipeline
is of typePipelineBuilder<Frame, Frame>
.Trying to add an
InboundChannelHandler
which does not consumesFrame
s will fail to compile. The Pipeline can then be added to a realChannelPipeline
by callingchannel.pipeline.addHandlers(pipeline)
.You can find a prototype implementation in the RSocket repository with a a real world example: https://github.com/rsocket/rsocket-swift/blob/5ec54700b3a00a734e4c31962dc72979339e75df/Sources/RSocketCore/ChannelPipeline.swift#L20-L172
What do you think? Worth adding something like this to SwiftNIO? Found some old draft PR which also tries to make SwiftNIO more type-safe but more focused on the channel handler side and not on the pipeline.
Note: Just played around with it for ~1 hour so no real world usage experience yet.