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. Theinitializer
contains a where clause which refers toplaceholder T
, so I need to move theplaceholder T
into protocol as anassociated 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 theinitializer function
because theAnyPipeline<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 theinitializer
likeinit<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 aSource
and passes it to aProcedure
.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 theAnySource
orAnyProcedure
type erasures.A simple implementation of this wrapper would be:
This leaves you free to add the initialiser to your
Pipeline
protocol, as it need not concern itself with what the actualDataType
is – only that the source and procedure must have the sameDataType
: