Type erasure works when you have different "implementors" for a protocol with an associated type but each of them uses the same concrete associated type.
For example, with this PAT with some of its concrete implementations in String:
protocol PAT {
associatedtype T
func process(_ t: T)
}
struct ImplV1: PAT {
typealias T = String
func process(_ t: String) {
//..
}
}
class ImplV2: PAT {
typealias T = String
func process(_ t: String) {
//..
}
}
If you would be interested in only being able to process Strings , without concerning with whichever is the actual type that implements the protocol you can abstract that detail away using type erasure like so:
struct AnyPATImpl<T>: PAT {
private let processFn: (T) -> Void
init<Implementor: PAT>(_ instance: Implementor) where Implementor.T == T {
processFn = instance.process(_:)
}
func process(_ t: T) {
processFn(t)
}
}
enum StringImplVersion {
case v1
case v2
}
struct StringImplementationHandler {
let implementor: AnyPATImpl<String>
init(which: StringImplVersion) {
switch which {
case .v1:
implementor = .init(ImplV1())
case .v2:
implementor = .init(ImplV2())
}
}
func processBatch(_ strings: [String]) {
for s in strings { implementor.process(s) }
}
}
Your case is different however. You have two implementors for StorageProvider but there's no commonality between them except that their associatedType conforms to StoragePath. This constraint may help you when writing extensions for that protocol, or manipulating an instance of a type constrained to that protocol, i.e:
extension StorageProvider {
func printStoragePath() {
print(root.read())
}
}
func useStorageProvider<S: StorageProvider>(_ s: S) {
print(s.root.read())
}
But it will not be enough, with the current swift version(?), to leverage an abstraction between LocalStorageProvider and RemoteStorageProvider
And keep in mind that a StorageProvider still wants an actual type in its readFile(atPath:) method, i.e:
// This will not work
func useStorageProvider<S: StorageProvider>(_ s: S, path: StoragePath) {
print(s.readFile(atPath: path)
}
Ultimately you need to think of what type information is actually of interest vs not-of-interest for each of your use cases.
With whats written in your current example, in the ContentProvider.read(path:) method you are only interested in the ContentProvider.provider being able to read the file from its own .root property and return the contents of it. The actual types of the root and provider are not if interest in this case, so you can make an abstraction for that in the following manner:
struct AnyGenericStorageProvider {
private let readFileFromOwnRootFn: () -> String
init<T: StorageProvider>(_ t: T) {
readFileFromOwnRootFn = {
return t.readFile(atPath: t.root)
}
}
func readFileFromOwnRoot() -> String {
return readFileFromOwnRootFn()
}
}
class ContentProvider {
let provider: AnyGenericStorageProvider
init(type: StorageType) {
switch type {
case .local:
self.provider = .init(LocalStorageProvider())
case .remote:
self.provider = .init(RemoteStorageProvider())
}
}
func read(path: String) -> String {
return provider.readFileFromOwnRoot()
}
}