Home > Software engineering >  How to conform to a protocol with a generic superscript affordance in Swift?
How to conform to a protocol with a generic superscript affordance in Swift?

Time:08-14

TL;DR How can I conform to the supscript function of a protocol in my implementation?

Protocol:

protocol DataStore {
    //...
    subscript<T>(id: T.ID) -> T where T: Identifiable { get set }
}

Neither

subscript<T>(id: T.ID) -> T where T : Identifiable {
        get { Project() }//just to return anything for the time being…
        set {}
    }

nor

subscript(id: Task.ID) -> Task {
        get { Project() }//just to return anything for the time being…
        set {}
    }

work...

The details:

I have developed a habit of creating specific data stores for my models. They all have the same functionality. A specific example could look like this:

final class ProjectDataStore: ObservableObject {
    {
        static let shared = ProjectDataStore()
        
        let persistence = AtPersistLocally.shared // that's from a personal package I made that handles saving and loading locally as json files
        
        @Published var projects: [Project] {
            didSet { //save }
        }
        
        private init(projects: [Project]? = nil) {
            //load from persistence
        }
        
        subscript(id: Project.ID) -> Project? {
            get { //return project with id }
            set { //set project at id }
        }
        
        func getBinding(by id: Project.ID) -> Binding<Project> {
            //return Binding
        }
        
        func getProjectBy(taskId: Task.ID) -> Project {
            //return instance
        }
        
        private func getIndex(by id: Project.ID) -> Int? {
            //return  index in array
        }
        
        private func load() -> [Project] {
            //load from persistence
        }
        
        private func save() {
            //save from persistence
        }
}

While this works as expected, I'd like to be able to introduce a protocol that I could when adding new functionality / models to have a blueprint on what's necessary for my DataStore.

Here is my first attempt:

protocol DataStore {
    static var shared: Self { get }
}

extension DataStore {
    var persistence: AtPersistLocally {
        get {
            AtPersistLocally.shared
        }
    }
}

To also conform to ObservableObject, I introduce a typealias

typealias ObservableObjectDataStore = DataStore & ObservableObject

and change my model to conform to this new typealias:

final class ProjectDataStore: ObservableObjectDataStore {
//...
}

With this, I already have a static instance and access to persistence available. Next step of course is to move more and more properties to the protocol–which is what I am struggling with right now.

Let's look at superscript first of all: I guess I understand what needs to be added to the protocol:

protocol DataStore {
    //...
    subscript<T>(id: T.ID) -> T where T: Identifiable { get set }
}

My problem now is that I don't know how to go about conforming to this subscript now while also getting access to a concrete model from the generic T from the implementation. This attempt…

final class ProjectDataStore: ObservableObjectDataStore {
    //...
    subscript<T>(id: T.ID) -> T where T : Identifiable {
        get { Project() }//just to return anything for the time being…
        set {}
    }
}

…leads to the error message Cannot convert return expression of type 'Task' to return type 'T'.

If I go with…

final class ProjectDataStore: ObservableObjectDataStore {
    //...
    subscript(id: Task.ID) -> Task {
        get { Project() }//just to return anything for the time being…
        set {}
    }
}

…the error message changes to Type 'TaskDataStore' does not conform to protocol 'DataStore'.

So I guess basically what I am asking is: how can I conform to my protocol's generic superscript in my implementation of ProjectDataStore?

I have a feeling that I am not too far of, but a critical info is obviously missing…

CodePudding user response:

subscript<T>(id: T.ID) -> T where T: Identifiable { get set }

This says that the caller may pick any Identifiable T, and this method promise return a value of that type. This can't be implemented (other than calling fatalError() and crashing). Identifiable doesn't even promise that the type has an init. Your protocol is impossible.

What you probably meant to write is, which says that a given DataStore will return some specific Identifiable type (not "whatever type the caller requests"):

protocol DataStore {
    associatedType Item: Identifiable
    subscript(id: Item.ID) -> Item { get set }
}

But I expect this would be much better implemented without a protocol, and just use a generic struct:

struct DataStore<Item: Identifiable> { ... }
  • Related