Protocol Method Dispatch for Generic Class

Hello.

Please look at my code below. The problem is about method dispatch. I have to say that such explicit type casting looks very ugly. Is there any other case to call configure() for a particular generic class without type casting? In a real project, I've got about 40 classes. This is very annoying (leads to bugs) to write

else if T.self == IntroductionProperties.self {
        (self as? Configurator<IntroductionProperties>)?.configure()
}

for each class.

I want to ask the people who develop this language. Is it a good practice to write such code?

// MARK: - Properties

protocol ModuleProperties { }

class BackupCardProperties { }
extension BackupCardProperties: ModuleProperties { }

class IntroductionProperties { }
extension IntroductionProperties: ModuleProperties { }

// MARK: - Configurator

protocol ModuleConfigurator {
    func configure()
}
class Configurator<T> where T: ModuleProperties {

    var properties: T

    init(properties: T) {
        self.properties = properties
    }
}

extension Configurator: ModuleConfigurator {
    func configure() {
        print("Configurator extension")
        if T.self == BackupCardProperties.self {
            (self as? Configurator<BackupCardProperties>)?.configure()
        } else if T.self == IntroductionProperties.self {
            (self as? Configurator<IntroductionProperties>)?.configure()
        } else {
            fatalError("not implemented")
        }
    }
}

extension Configurator where T == BackupCardProperties {
    func configure() {
        print("Configurator of BackupCardProperties")
    }
}

extension Configurator where T == IntroductionProperties {
    func configure() {
        print("Configurator of IntroductionProperties")
    }
}


// MARK: - Example

let configurators: [ModuleConfigurator] = [Configurator(properties: BackupCardProperties()),
                                           Configurator(properties: IntroductionProperties())]

configurators.forEach {
    $0.configure()
}

The result is

Configurator extension
Configurator of BackupCardProperties
Configurator extension
Configurator of IntroductionProperties

Assuming ModuleProperties doesn't contain associated types, you should be able to avoid Generics here:

class Configurator {
    var properties: ModuleProperties

    init(properties: ModuleProperties) {
        self.properties = properties
    }
}

extension Configurator: ModuleConfigurator {
    func configure() {
        self.properties.configure()
    }
}

If you do have associated types or self requirements, then I would look into Type Erasure. In either case, you should just be able to call self.properties.configure()

I don't want to implement 'configure' in simple 'properties' struct / class. It just contains a piece of data.

In that case, you might be able to get away with dropping the main ModuleConfigurator conformance and just use conditional conformances:

extension Configurator: ModuleConfigurator where T == BackupCardProperties {
    func configure() {
        print("Configurator of BackupCardProperties")
    }
}
//etc...

I haven't tried it though, so it may not work...

This still seems like a terrible way to organize your code. I am not sure what is going on in configure(), but if it only affects the properties' model code, it makes sense to keep that organized with each model type. Someone else may have a better idea though.

such code doesn't work if you implement several extensions

extension Configurator: ModuleConfigurator where T == BackupCardProperties {

and

extension Configurator: ModuleConfigurator where T == IntroductionProperties {

this is impossible. It works only for one extension.

The only ways to get dynamic dispatch in Swift are

  • subclassing / overrides
  • protocol dispatch
  • calling through a function value (obviously dynamic)

In this case, you want something that depends on the type of T: ModuleProperties; the way to get that is to put the requirement on ModuleProperties.

// MARK: - Properties

protocol ModuleProperties {
  static func configure(_ configurator: Configurator<Self>)
}
extension ModuleProperties {
  static func configure(_ configurator: Configurator<Self>) {
    print("Default configurator (do nothing)")
  }
}

final class BackupCardProperties { }
extension BackupCardProperties: ModuleProperties { }

final class IntroductionProperties { }
extension IntroductionProperties: ModuleProperties { }

// MARK: - Configurator

protocol ModuleConfigurator {
    func configure()
}
class Configurator<T> where T: ModuleProperties {

    var properties: T

    init(properties: T) {
        self.properties = properties
    }
}

extension Configurator: ModuleConfigurator {
    func configure() {
        print("Configurator extension")
        T.configure(self)
    }
}

extension BackupCardProperties {
    static func configure(_ configurator: Configurator<BackupCardProperties>) {
        print("Configurator of BackupCardProperties")
    }
}

extension IntroductionProperties {
    static func configure(_ configurator: Configurator<IntroductionProperties>) {
        print("Configurator of IntroductionProperties")
    }
}


// MARK: - Example

let configurators: [ModuleConfigurator] = [Configurator(properties: BackupCardProperties()),
                                           Configurator(properties: IntroductionProperties())]

configurators.forEach {
    $0.configure()
}

I can see how this feels a bit backwards, though. Still, there's not an obvious design for a different language feature that leaves the method on Configurator.

2 Likes

Thanks. Will Swift Team implement such dispatch feature?

I think there are probably lots of other things that are more important to get to first, especially when we don't have a design for whatever this feature would be yet and there is a way to accomplish the same thing. But the forums are the right place to try to work out what such a feature would look like and how it might be implemented.