Hi, I ran into a design issue with regarding to Swift not allowing Array
of different Element
types to conform to the same protocol multiple times. I wonder how to make an alternative design. Thanks for any help.
My scenario: my app has two entities Foo
and Bar
. They are saved in separate arrays. I extend [Foo]
and [Bar]
to add methods to create/modify/delete a single instance. Because Foo
and Bar
have different details, they don't share code in those CRUD methods.
Below are the initial code, which contain only APIs working on single instance. The code are straightforward.
struct Foo {
var id: UUID
var x: Int
}
struct Bar {
var id: UUID
var y: String
}
typealias FooStorage = [Foo]
typealias BarStorage = [Bar]
extension FooStorage {
mutating func modify(id: UUID, newValue: Int) {
guard let index = firstIndex(where: { $0.id == id }) else { return }
self[index].x = newValue
}
}
extension BarStorage {
mutating func modify(id: UUID, newValue: String) {
guard let index = firstIndex(where: { $0.id == id }) else { return }
self[index].y = newValue
}
}
The question is about how to implement APIs that process multiple instances in batch. At first I implement them for Foo
and Bar
separately, but I noticed I did a lot of copy and paste. So I tried to share the multiple instances API code between Foo
and Bar
. Below is how I implement it (note I modified the single instance api code a bit). The code almost works, excep that it fails to compile. See the error message at the end of the code.
struct Foo {
var id: UUID
var x: Int
}
struct Bar {
var id: UUID
var y: String
}
typealias FooStorage = [Foo]
typealias BarStorage = [Bar]
extension FooStorage {
mutating func modify(id: UUID, newValue: Int) {
guard let index = getIndex(id: id) else { return }
_modify(index: index, newValue: newValue)
}
mutating func _modify(index: Index, newValue: Int) {
self[index].x = newValue
}
func getIndex(id: UUID) -> Index? {
firstIndex(where: { $0.id == id })
}
}
extension BarStorage {
mutating func modify(id: UUID, newValue: String) {
guard let index = getIndex(id: id) else { return }
_modify(index: index, newValue: newValue)
}
mutating func _modify(index: Index, newValue: String) {
self[index].y = newValue
}
func getIndex(id: UUID) -> Index? {
firstIndex(where: { $0.id == id })
}
}
protocol BatchProcessing: Collection where Index: Strideable, Index.Stride: SignedInteger {
associatedtype Value
// This is the single instance crud api (its internal version). It's implemented manually. It will be used to implement the batch processing api.
mutating func _modify(index: Index, newValue: Value)
// This is batch processing api. It's to be implemented in the protocol extension. That's how we share the code between Foo and Bar.
mutating func batchModify(firstID: UUID, newValue: Value)
// helper util
func getIndex(id: UUID) -> Index?
}
extension BatchProcessing {
mutating func batchModify(firstID: UUID, newValue: Value) {
guard var index = getIndex(id: firstID) else { return }
while index < endIndex {
_modify(index: index, newValue: newValue)
index = self.index(after: index)
}
}
}
extension FooStorage: BatchProcessing {
typealias Value = Int
}
extension BarStorage: BatchProcessing {
typealias Value = String
}
// Error: Conflicting conformance of 'Array<Element>' to protocol 'BatchProcessing'; there cannot be more than one conformance, even with different conditional bounds
My first thought was to change the batchModify()
func to a generic function in Collection
protocol, but then I realized it wouldn't work, because it depends on _modify()
and, to express the dependence, I need to use protocol.
It seems the only option is to define FooStroage
and BarStorage
as two separate structs wrapping [Foo]
and [Bar]
, respectively. I think that should get rid of the compiler error, but on the other hand, introducing the wrapper types will make the code unnecessarily verbose.
Does anyone have any suggestion? Thanks.