swiftgenericsprotocols

Using protocol with typealias as a property


I have a protocol with a typealias:

protocol Archivable {
    typealias DataType
    
    func save(data: DataType, withNewName newName: String) throws
    func load(fromFileName fileName: String) throws -> DataType
}

and a class that conforms to that protocol:

class Archiver: Archivable {
    typealias DataType = Int
    
    func save(data: DataType, withNewName newName: String) throws {
        // saving
    }
    
    func load(fromFileName fileName: String) throws -> DataType {
        // loading
    }
}

and I would like to use Archivable as a property in another class like:

class TestClass {
    let archiver: Archivable = Archiver() //error here: Protocol 'Archivable' can only be used as a generic constraint because it has Self or associated type requirements
}

but it fails with

Protocol 'Archivable' can only be used as a generic constraint because it has Self or associated type requiments

My goal is that TestClass should only see Archiver as Archiveable, so if I want to change the saving/loading mechanism, I just have to create a new class that conforms to Archivable as set it as the property in TestClass, but I don't know if this is poosible, and if so, then how.

And I would like to avoid using AnyObject instead of DataType.


Solution

  • Depending on what you are actually trying to do, this can work using type erasure. If you follow the instructions in the link R Menke posted in the comments, you can achieve what you are trying to do. Since your property in TestClass seems to be a let, I'm going to assume you already know the type of DataType at compile time. First you need to setup a type erased Archivable class like so:

    class AnyArchiver<T>: Archivable {
        private let _save: ((T, String) throws -> Void)
        private let _load: (String throws -> T)
    
        init<U: Archivable where U.DataType == T>(_ archiver: U) {
            _save = archiver.save
            _load = archiver.load
        }
    
        func save(data: T, withNewName newName: String) throws {
            try _save(data, newName)
        }
    
        func load(fromFileName fileName: String) throws -> T {
            return try _load(fileName)
        }
    }
    

    Much like Swift's AnySequence, you'll be able to wrap your Archiver in this class in your TestClass like so:

    class TestClass {
        let archiver = AnyArchiver(Archiver())
    }
    

    Through type inference, Swift will type TestClass' archiver let constant as an AnyArchiver<Int>. Doing it this way will make sure you don't have to create a dozen protocols to define what DataType is like StringArchiver, ArrayArchiver, IntArchiver, etc. Instead, you can opt in to defining your variables with generics like this:

    let intArchiver: AnyArchiver<Int>
    let stringArchiver: AnyArchiver<String>
    let modelArchiver: AnyArchiver<Model>
    

    rather than duplicating code like this:

    protocol IntArchivable: Archivable {
        func save(data: Int, withNewName newName: String) throws
        func load(fromFileName fileName: String) throws -> Int
    }
    protocol StringArchivable: Archivable {
        func save(data: String, withNewName newName: String) throws
        func load(fromFileName fileName: String) throws -> String
    }
    protocol ModelArchivable: Archivable {
        func save(data: Model, withNewName newName: String) throws
        func load(fromFileName fileName: String) throws -> Model
    }
    
    let intArchiver: IntArchivable
    let stringArchiver: StringArchivable
    let modelArchiver: ModelArchivable
    

    I wrote a post on this that goes into even more detail in case you run into any problems with this approach. I hope this helps!