Stored property of type "protocol with associated type"

I have a couple of protocols describing a storage provider:

protocol StorageProvider {
    associatedtype PathType: StoragePath

    var root: PathType { get }

    func readFile(atPath path: PathType) -> String
}

protocol StoragePath {
    func read() -> String
}

enum StorageType {
    case local
    case remote
    // ...
}

with 2 concrete implementations:

struct LocalStoragePath: StoragePath {
    func read() -> String {
        return "local"
    }
}

struct LocalStorageProvider: StorageProvider {
    typealias PathType = LocalStoragePath

    var root: LocalStoragePath {
        return LocalStoragePath()
    }

    func readFile(atPath path: LocalStoragePath) -> String {
        return path.read()
    }
}
struct RemoteStoragePath: StoragePath {
    func read() -> String {
        return "remote"
    }
}


struct RemoteStorageProvider: StorageProvider {
    typealias PathType = RemoteStoragePath

    var root: RemoteStoragePath {
        RemoteStoragePath()
    }

    func readFile(atPath path: RemoteStoragePath) -> String {
        return path.read()
    }
}

and a class tying everthing together:

class ContentProvider {
    let provider: Any

    init(type: StorageType) {
        switch type {
        case .local:
            self.provider = LocalStorageProvider()
        case .remote:
            self.provider = RemoteStorageProvider()
        }
    }

    func read(path: String) -> String {
        if let p = self.provider as? LocalStorageProvider {
            return p.readFile(atPath: p.root)
        }

        if let p = self.provider as? RemoteStorageProvider {
            return p.readFile(atPath: p.root)
        }

        abort()
    }
}

However, I am not happy that I have to use type Any for the provider property. I read up on documentation and ended up with a type erased generic storage provider:

class GenericStorageProvider<T: StoragePath>: StorageProvider {
    var root: T {
        return self._root
    }

    typealias PathType = T
    private let _readFile: (T) -> String
    private let _root: T

    init<U: StorageProvider>(_ provider: U) where U.PathType == T {
        self._readFile = provider.readFile
        self._root = provider.root
    }

    func readFile(atPath path: T) -> String {
        return self._readFile(path)
    }
}

but that also doesn't work because the provider property in ContentProvider cannot be a GenericStorageProvider, it has to be GenericStorageProvider<T>.

Is there any other way that I am not aware of or does this simply not work as I intended? Besides the initializer in ContentProvider everything is under my control.

Any help is much appreciated

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()
    }
}
Terms of Service

Privacy Policy

Cookie Policy