Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to have an array of instances which take a generic parameter without knowing (or caring) what the parameter is?

Tags:

Consider the following test case, which contains a 'factory' class which is able to call a closure it contains, providing a new instance of some 'defaultable' type:

protocol Defaultable {
    init()
}

extension Int:    Defaultable { }
extension Double: Defaultable { }
extension String: Defaultable { }

class Factory<T : Defaultable> {
    let resultHandler: (T) -> ()

    init(resultHandler: (T) -> ()) {
        self.resultHandler = resultHandler
    }

    func callResultHandler() {
        resultHandler(T.init())
    }
}

Now, this works well when I use it on its own, where I can keep track of the generic type:

// Create Int factory variant...
let integerFactory = Factory(resultHandler: { (i: Int) in print("The default integer is \(i)") })

// Call factory variant...
integerFactory.callResultHandler()

Unfortunately, it doesn't work so well if I want to use factories in a way where I can't keep track of the generic type:

// Create a queue of factories of some unknown generic type...
var factoryQueue = [Factory]()

// Add factories to the queue...
factoryQueue.append(integerFactory)
factoryQueue.append(doubleFactory)
factoryQueue.append(stringFactory)

// Call the handler for each factory...
for factory in factoryQueue {
    factory.callResultHandler()
}

I understand the error I get (Generic parameter 'T' could not be inferred), but I don't understand why I can't do this, because when I interact with the array, I don't need to know what the generic parameter is (I don't interact with any of the generic things in the Factory instance). Is there any way I can achieve the above?

Note that the above is a simplified example of what I'm trying to do; in actuality I'm designing a download manager where it can infer what type of file I want (JSON, image, etc.) using generics; the protocol actually contains an init(data:) throws initialiser instead. I want to be able to add the download objects to a queue, but I can't think of any way of adding them to a queue because of the generic nature of the download objects.

like image 727
Robert Avatar asked Apr 27 '16 10:04

Robert


1 Answers

The problem is that Swift's strict type safety means you cannot mix two instances of the same class with different generic parameters. They are effectively seen as completely different types.

However in your case, all you're doing is passing a closure to a Factory instance that takes a T input, and then invoking it at any given time with T.init(). Therefore, you can create a closed system in order to contain the type of T, meaning that you don't actually need your generic parameter to be at the scope of your class. You can instead restrict it to just the scope of the initialiser.

You can do this by defining your resultHandler as a Void->Void closure, and create it by wrapping the passed closure in the initialiser with another closure – and then passing in T.init() into the closure provided (ensuring a new instance is created on each invocation).

Now whenever you call your resultHandler, it will create a new instance of the type you define in the closure that you pass in – and pass that instance to the closure.

This doesn't break Swift's type safety rules, as the result of T.init() is still known due to the explicit typing in the closure you pass. This new instance is then being passed into your closure that has a matching input type. Also, because you never pass the result of T.init() to the outside world, you never have to expose the type in your Factory class definition.

As your Factory class itself no longer has a generic parameter, you can mix different instances of it together freely.

For example:

class Factory {
    let resultHandler: () -> ()

    init<T:Defaultable>(resultHandler: (T) -> ()) {
        self.resultHandler = {
            resultHandler(T.init())
        }
    }

    func callResultHandler() {
        resultHandler()
    }
}

// Create Int factory variant...
let integerFactory = Factory(resultHandler: { (i: Int) in debugPrint(i) })

// Create String factory variant...
let stringFactory = Factory(resultHandler: { (i: String) in debugPrint(i) })

// Create a queue of factories of some unknown generic type...
var factoryQueue = [Factory]()

// Add factories to the queue...
factoryQueue.append(integerFactory)
factoryQueue.append(stringFactory)

// Call the handler for each factory...
for factory in factoryQueue {
    factory.callResultHandler()
}

// prints:
// 0
// ""

In order to adapt this to take an NSData input, you can simply modify the resultHandler closure & callResultHandler() function to take an NSData input. You then just have to modify the wrapped closure in your initialiser to use your init(data:) throws initialiser, and convert the result to an optional or do your own error handling to deal with the fact that it can throw.

For example:

class Factory {
    let resultHandler: (NSData) -> ()

    init<T:Defaultable>(resultHandler: (T?) -> ()) {
        self.resultHandler = {data in
            resultHandler(try? T.init(data:data)) // do custom error handling here if you wish
        }
    }

    func callResultHandler(data:NSData) {
        resultHandler(data)
    }
}
like image 183
Hamish Avatar answered Sep 28 '22 01:09

Hamish