Robert Robert - 7 months ago 20
Swift Question

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

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.

Answer

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, you don't actually need your generic parameter to be at the scope of your class (as you never actually need to use it in that scope). You can instead use it only in the scope of the initialiser.

You can then define 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.

This now means that 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)
    }
}
Comments