1

I have a pretty complicated structure with Generic types in my app. This works, but there is an issue, that at the end of this chain, I need to specify some types 2 times, because they need to be used as generics of some class, and one of these generic types, also need generic type. Which are always the same types as these before it. It goes like this <A, B, C<A, B>>

This makes it a little unpleasant to use. Is there some way to make it infer A and B from C Here is sample code, with stripped functionalities:

// MARK: - Base classes that Im using, stripped from funcionalities.

// This one is a base for performing some detection. It can return any type as a result of scanning.
class DetectionPerformer<ResultType> {}

// This one adds possibility to load some model needed to perform scanning from the disk.
class LocalFileDetectionPerformer<ResultType, LocalModelType>: DetectionPerformer<ResultType> {
    required init(localModelURL: URL) {}
}

// This one adds possibility to download this model and store it on the disk before loading.
class DownloadableDetectionPerformer<ResultType, LocalModelType>: LocalFileDetectionPerformer<ResultType, LocalModelType> {}

// This one wraps LocalFileDetectionPerformer inside DownloadableDetectionPerformer, and use them together.
class RemoteFileDetectionPerformer<ResultType, LocalModelType, LocalFileDetectionPerformerType: DownloadableDetectionPerformer<ResultType, LocalModelType>>: DetectionPerformer<ResultType> {
    
    private let localFileDetectionPerformer: LocalFileDetectionPerformerType
    
    init(remoteModelURL: URL) {
        let localModelURL = Self.localModelURL(for: remoteModelURL)
        localFileDetectionPerformer = LocalFileDetectionPerformerType(localModelURL: localModelURL)
    }
    
    static func localModelURL(for url: URL) -> URL {
        url.appendingPathExtension("local")
    }
}

// Detector is main object in application. It takes some type of Detector as init parameter, and works on it.
class Detector<ResultType, DetectionPerformerType: DetectionPerformer<ResultType>> {
    let performer: DetectionPerformerType

    init(performer: DetectionPerformerType) {
        self.performer = performer
    }
}

// Now I can implement some specific performers, whcich will do real work. For example:
class SamplePerformer: DownloadableDetectionPerformer<Int, String> {}

// And I'm able to create Detector with any type of Performer:

let detectorA = Detector(performer: SamplePerformer(localModelURL: URL(string: "")!))

// The problem begins, when I want to wrap Performer into RemoteFileDetectionPerformer

let detectorB = Detector(performer: RemoteFileDetectionPerformer<Int, String, SamplePerformer>(remoteModelURL: URL(string: "")!))

// Here I need to specify all 3 generic types of RemoteFileDetectionPerformer, even tough two first are always the same as generic types of SamplePerformer. I can't even specify different ones, as this would create an error.
// Is there some way for RemoteFileDetectionPerformer to infer these first two generic types from LocalFileDetectionPerformerType? Maybe I need to construct these some differently?
Damian Dudycz
  • 2,622
  • 19
  • 38

2 Answers2

2

I feel like the classes you showed in the first half of the code block should be protocols instead. That is, DetectionPerformer, LocalFileDetectionPerformer, DownloadableDetectionPerformer should all be protocols. They don't seem to have any real implementation in them, as is evident in your comment "Now I can implement some specific performers, which will do real work". If you have any implementations that you want to put in them, you can put it in an extension most of the time. Why making them protocols solves the problem? Because then we can use associated types rather than type parameters.

protocol DetectionPerformer {
    associatedtype ResultType
}

// This one adds possibility to load some model needed to perform scanning from the disk.
protocol LocalFileDetectionPerformer: DetectionPerformer {
    associatedtype LocalModelType
    init(localModelURL: URL)
}

// This one adds possibility to download this model and store it on the disk before loading.
protocol DownloadableDetectionPerformer: LocalFileDetectionPerformer {}

// This one wraps LocalFileDetectionPerformer inside DownloadableDetectionPerformer, and use them together.
class RemoteFileDetectionPerformer<LocalFileDetectionPerformerType: DownloadableDetectionPerformer>: DetectionPerformer {
    typealias ResultType = LocalFileDetectionPerformerType.ResultType
    private let localFileDetectionPerformer: LocalFileDetectionPerformerType
    
    init(remoteModelURL: URL) {
        let localModelURL = Self.localModelURL(for: remoteModelURL)
        localFileDetectionPerformer = LocalFileDetectionPerformerType(localModelURL: localModelURL)
    }
    
    static func localModelURL(for url: URL) -> URL {
        url.appendingPathExtension("local")
    }
}

class Detector<DetectionPerformerType: DetectionPerformer> {
    let performer: DetectionPerformerType

    init(performer: DetectionPerformerType) {
        self.performer = performer
    }
}

class SamplePerformer: DownloadableDetectionPerformer {
    required init(localModelURL: URL) {
        
    }
    
    typealias ResultType = Int
    typealias LocalModelType = String
}

This allows you to do:

let detectorB = Detector(performer: RemoteFileDetectionPerformer<SamplePerformer>(remoteModelURL: URL(string: "")!))
Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • Thanks, you right, this is a better design. These classes do have a lot more functionalities, I just stripped them to show the problem. But from first tries, I guess I will probably be able to convert them to protocols and just add their stored properties to the implementations while adding most of the functions to the extensions. What helped me the most is the idea, to create a typealias from Generic type subtypes (like this typealias ResultType = LocalFileDetectionPerformerType.ResultType). This will be a great improvement. Thanks. – Damian Dudycz Jul 30 '20 at 17:20
  • I have one more question. How could I create an array of Elements, that are Detectors with Performers returning some specified type. Something like let a = [Detector>](). So that later I could use a loop on this array, and execute on all its elements some generic function that returns ResultType. – Damian Dudycz Jul 30 '20 at 17:50
  • Well, you lose the ability to do that :-(. [Protocols don’t conform to themselves](https://stackoverflow.com/questions/33112559/protocol-doesnt-conform-to-itself), among other reasons. @DamianDudycz – Sweeper Jul 30 '20 at 23:19
  • @DamianDudycz Actually you could create an `AnyDetectionPerformer` type eraser and use a `[Detector>]`. I can't show you how to do it unless you show the other requirements of `DetectionPerformer`, but [this](https://medium.com/@chris_dus/type-erasure-in-swift-84480c807534) should help. – Sweeper Jul 30 '20 at 23:56
  • Thank you, I'll try this. I just tried to do this now, bo didn't manage to do this, but this was just a short first try. If you want to take a look at the requirements of DetectionPerformer, you can see all the code here https://github.com/damiandudycz/ComputerVision . It can be installed as swift package – Damian Dudycz Jul 31 '20 at 05:56
0

First, I strongly agree with Sweeper. This is almost certainly a poor use of class inheritance and generics in Swift. Generally, if you think you want class inheritance, you should reach for other tools first. First, composition (could you just pass functions or bundles of functions). And then protocols. Looking at your types, it feels like you should be splitting up "thing that fetches data" from "thing that detects result in data" and composing the two.

That said, the problem is general and very legitimate, and there is a solution. Pass the type as a parameter:

init(performer: LocalFileDetectionPerformerType.Type, remoteModelURL: URL) { ... }

Then, when you call it, rather than explicitly type-specifying it, pass the type:

let detectorB =
    Detector(performer: RemoteFileDetectionPerformer(performer: SamplePerformer.self,
                                                     remoteModelURL: URL(string: "https://example.com")!))

The type will automatically be worked out:

Detector<Int, RemoteFileDetectionPerformer<Int, String, SamplePerformer>>

If there are cases where the type would be known from context by other means, you can add a default parameter:

init(performer: LocalFileDetectionPerformerType.Type = LocalFileDetectionPerformerType.self, ...

Then you can leave off the parameter when it's not necessary.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610