Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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 arciver: Archivable = Archiver() //error here: Protocol 'Archivable' can only be used as a generic constraint because it has Self or associated type requiments
}

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.

like image 947
Dániel Nagy Avatar asked Dec 06 '15 16:12

Dániel Nagy


People also ask

CAN protocol have properties?

A protocol can have properties as well as methods that a class, enum or struct conforming to this protocol can implement. A protocol declaration only specifies the required property name and type. It doesn't say anything about whether the property should be a stored one or a computed one.

What is protocol property?

Protocol properties are independent of any particular data record; they exist outside the flow of data. Such properties can be defined and scoped at a number of levels, reflecting the nesting of protocols, subprotocols, and components.

How do I use Typealias in Swift?

A type alias allows you to provide a new name for an existing data type into your program. After a type alias is declared, the aliased name can be used instead of the existing type throughout the program. Type alias do not create new types. They simply provide a new name to an existing type.

What is the use of Typealias?

typealias can be useful for reducing verbosity and complexity and improving readability and clarity. typealias is especially powerful when working with complex closure types and those conforming to multiple protocols.


2 Answers

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!

like image 166
Hector Matos Avatar answered Oct 03 '22 11:10

Hector Matos


When you try to declare and assign archiver:

let archiver: Archivable = Archiver()

it must have concrete type. Archivable is not concrete type because it's protocol with associated type.

From "The Swift Programming Language (Swift 2)" book:

An associated type gives a placeholder name (or alias) to a type that is used as part of the protocol. The actual type to use for that associated type is not specified until the protocol is adopted.

So you need to declare protocol that inherits from Archivable and specifies associated type:

protocol IntArchivable: Archivable {
    func save(data: Int, withNewName newName: String) throws

    func load(fromFileName fileName: String) throws -> Int
}

And then you can adopt this protocol:

class Archiver: IntArchivable {
    func save(data: Int, withNewName newName: String) throws {
        //saving
    }

    func load(fromFileName fileName: String) throws -> Int {
        //loading
    }
}

There are no truly generic protocols in Swift now so you can not declare archiver like this:

let archiver: Archivable<Int> = Archiver()

But the thing is that you do not need to do so and I explain why.

From "The Swift Programming Language (Swift 2)" book:

A protocol defines a blueprint of methods, properties, and other requirements that suit a particular task or piece of functionality.

So basically when you want to declare archiver as Archivable<Int> you mean that you don't want some piece of code using archiver to know about its concrete class and to have access to its other methods, properties, etc. It's obvious that this piece of code should be wrapped in separate class, method or function and archiver should be passed there as parameter and this class, method or function will be generic.

In your case TestClass can be generic if you pass archivable via initializer parameter:

class TestClass<T, A: Archivable where A.DataType == T> {
    private let archivable: A

    init(archivable: A) {
        self.archivable = archivable
    }

    func test(data: T) {
        try? archivable.save(data, withNewName: "Hello")
    }
}

or it can have generic method that accepts archivable as parameter:

class TestClass {        
    func test<T, A: Archivable where A.DataType == T>(data: T, archivable: A) {
        try? archivable.save(data, withNewName: "Hello")
    }
}
like image 20
mixel Avatar answered Oct 03 '22 10:10

mixel