Swift is amazing but not mature yet, so there are some compiler restrictions and among them is generic protocols. Generic protocols can't be used as a regular type annotation due to type-safety considerations. I found a workaround in a post by Hector Matos. Generic Protocols & Their Shortcomings
The main idea is use type erasure to convert a generic protocol into a generic class, and it's cool. But I was stuck when applying this tech to more complex scenarios.
Assume there is an abstract Source, which produces data, and an abstract Procedure, which processes that data, and a pipeline, which combines a source and a procedure whose data types are matched.
protocol Source {
associatedtype DataType
func newData() -> DataType
}
protocol Procedure {
associatedtype DataType
func process(data: DataType)
}
protocol Pipeline {
func exec() // The execution may differ
}
And the I want the client code to be simple as:
class Client {
private let pipeline: Pipeline
init(pipeline: Pipeline) {
self.pipeline = pipeline
}
func work() {
pipeline.exec()
}
}
// Assume there are two implementation of Source and Procedure,
// SourceImpl and ProcedureImpl, whose DataType are identical.
// And also an implementation of Pipeline -- PipelineImpl
Client(pipeline: PipelineImpl(source: SourceImpl(), procedure: ProcedureImpl())).work()
Implementing Source and Procedure is simple, since they are at the bottom of the dependecy:
class SourceImpl: Source {
func newData() -> Int { return 1 }
}
class ProcedureImpl: Procedure {
func process(data: Int) { print(data) }
}
The PITA appears when implementing Pipeline
// A concrete Pipeline need to store the Source and Procedure, and they're generic protocols, so a type erasure is needed
class AnySource<T>: Source {
private let _newData: () -> T
required init<S: Source>(_ source: S) where S.DataType == T {
_newData = source.newData
}
func newData() -> T { return _newData() }
}
class AnyProcedure<T>: Procedure {
// Similar to above.
}
class PipelineImpl<T>: Pipeline {
private let source: AnySource<T>
private let procedure: AnySource<T>
required init<S: Source, P: Procedure>(source: S, procedure: P) where S.DataType == T, P.DataType == T {
self.source = AnySource(source)
self.procedure = AnyProcedure(procedure)
}
func exec() {
procedure.process(data: source.newData())
}
}
Ugh, Actually this one works! Am I kidding you? No.
I am not satisfied with this one, because the initializer
of PipelineImpl
is quite generic, so I want it to be in the protocol (Am I wrong with this obsession?). And this leads to two end:
The protocol Pipeline
will be generic. The initializer
contains a where clause which refers to placeholder T
, so I need to move the placeholder T
into protocol as an associated type
. Then the protocol turns into a generic one, which means I can't use it directly in my client code -- may need another type erasure.
Although I can bear the troublesome of writing another type erasure for the Pipeline
protocol, I don't know how to deal with the initializer function
because the AnyPipeline<T>
class must implement the initializer regarding to the protocol but it's only a thunk class actually, which shouldn't implement any initializer itself.
Keep the protocol Pipeline
non-generic. With writing the initializer
like
init<S: Source, P: Procedure>(source: S, procedure: P)
where S.DataType == P.DataType
I can prevent the protocol being generic. This means the protocol only states that "Source and Procedure must have same DataType and I don't care what it is". This makes more sense but I failed to implement a concrete class confirming this protocol
class PipelineImpl<T>: Protocol {
private let source: AnySource<T>
private let procedure: AnyProcedure<T>
init<S: Source, P: Procedure>(source: S, procedure: P)
where S.DataType == P.DataType {
self.source = AnySource(source) // doesn't compile, because S is nothing to do with T
self.procedure = AnyProcedure(procedure) // doesn't compile as well
}
// If I add S.DataType == T, P.DataType == T condition to where clasue,
// the initializer won't confirm to the protocol and the compiler will complain as well
}
So, how could I deal with this?
Thanks for reading allll
this.
I think you're over-complicating this somewhat (unless I'm missing something) – your PipelineImpl
doesn't appear to be anything more than a wrapper for a function that takes data from a Source
and passes it to a Procedure
.
As such, it doesn't need to be generic, as the outside world doesn't need to know about the type of data being passed – it just needs to know that it can call exec()
. As a consequence, this also means that (for now at least) you don't need the AnySource
or AnyProcedure
type erasures.
A simple implementation of this wrapper would be:
struct PipelineImpl : Pipeline {
private let _exec : () -> Void
init<S : Source, P : Procedure>(source: S, procedure: P) where S.DataType == P.DataType {
_exec = { procedure.process(data: source.newData()) }
}
func exec() {
// do pre-work here (if any)
_exec()
// do post-work here (if any)
}
}
This leaves you free to add the initialiser to your Pipeline
protocol, as it need not concern itself with what the actual DataType
is – only that the source and procedure must have the same DataType
:
protocol Pipeline {
init<S : Source, P : Procedure>(source: S, procedure: P) where S.DataType == P.DataType
func exec() // The execution may differ
}