Epic fail - Swift protocols

programming   opinion   Swift   war stories  

It all started with what seemed like a simple thing, and indeed... would be if I was using Java. I had a view controller which manage a number of other view controllers. A very common scenario in any iOS app.

The view controller being managed are for displaying a detailed view of an object which I've instantiated from a server response. Again, a very common thing to do in any app that talks to a server.

So first of all I thought - 'I should use protocols to represent the server objects. That way I can swap in implementations or mock them for testing.' So I started with this:

protocol ModelObject {  
    associatedtype IdType
    var id:IdType? { get }
}

Now this by itself is not much use apart from indicating that all model objects have an id. In a real code base there would be other functionality attached. For example serialization code might be included. Or there might be other properties that are common to all model classes.

Next I added a protocol to represent the various properties and functions of an object from my server.

protocol Project {  
    var name:String? { get }
}

Again this is a stripped down protocol for the purposes of this article. But you get the idea - A protocol that defines a Project.

Pretty straight forward stuff. So next I created an actual class which defines a project as a model object:

class ProjectImpl:Project, ModelObject {  
    typealias IdType = Int
    var id:IdType?
    var name: String?
}

So now that I had my model, it was time to define the view controllers to display it. Lets start with an abstract parent view controller. Again - nothing new. All view controllers that will display the details of of ModelObject will have some common functionality.

class DetailsViewController<T>:UIViewController {  
    var modelObject:T?
}

And and project details view controller:

class ProjectDetailsViewController:DetailViewController<Project> {  
}

Notice I used the Project protocol here. The view controller doesn't need to know about the internal ModelObject stuff. So now we have all our base classes and protocols. Lets start the controller that manages all this.

class DetailsManagingViewController:UIViewController {  
    var controllers = [DetailsViewController<Any>]()
    override func viewDidLoad() {
        controllers = [ProjectDetailsViewController()]
    }
}

Protocols as generic types

Except that the above code won't compile. Here's the error:

error: cannot convert value of type 'ProjectDetailsViewController' to expected element type 'DetailsViewController<Any>'  
        controllers = [ProjectDetailsViewController()]

Which is a problem because I'm going to have other controllers which manage other model objects. I need to be able to store my project view controller without specifying it's type.

You can mess around with this endlessly. I know. I have. But the up shots is that unlike Java, where I would just do something like this:

class DetailsManagingViewController extends UIViewController {  
    List<DetailsViewController<?>> controllers;
    void viewDidLoad() {
        controllers = {new projectDetailsViewController()};
    } 
}

Swift has no concept of wildcard generics, Swift has to know everything up front because it's type resolution is run at compile type, not runtime. This creates a whole raft of problems for the developer. On one hand, Apple want's use to use protocols (which is a good practice) and compose everything. On the other hand they've pretty much neutered them from the get go by insisting on compile time resolution.

But lets see of we can work our way out of this problem. First lets try and fix this by adding a base model object class which can be used in the controllers to represent all model class. We'll also need to update ProjectImpl to inherit from it. ModelObjectImpl has to define id so we set it's type to Any because we'll override it in each actual type.

class ModelobjectImpl: ModelObject {  
    typealias IdType = Any
    var id:IdType?
}

class ProjectImpl:ModelobjectImpl, Project {  
    typealias IdType = Int
    var name: String?
}

And now we update the view controllers:

class DetailsViewController<T:ModelObjectImpl>:UIViewController {  
    var modelObject:T?
}

class ProjectDetailsViewController:DetailsViewController<ProjectImpl> {  
}

class DetailsManagingViewController:UIViewController {  
    var controllers = [DetailsViewController<ModelObjectImpl>]()
    override func viewDidLoad() {
        controllers = [ProjectDetailsViewController()]
    }
}

But there's still an error:

error: cannot convert value of type 'ProjectDetailsViewController' to expected element type 'DetailsViewController<ModelObjectImpl>'  
        controllers = [ProjectDetailsViewController()]

Which makes no sense. ProjectDetailsViewController inherits from DetailsViewController and ProjectImpl inherits from ModelObjectImpl so whats the problem? Lets try a cast:

controllers = [ProjectDetailsViewController() as DetailsViewController<ModelObjectImpl>]  

Nope, same error. I've played with this a bit and it appears that Swift is unable to handle the concept of a type that specified in a generic declaration matching a declared supertype. ie. it cannot resolve let Something<ModelObjectImpl> = Something<ProjectImpl>. If you want to try this out yourself, cut and paste this into a playground:

class X {}  
class Y:X {}  
class A<T> {}  
let a:A<X> = A<Y>()  

You'll get the same error.

But this is getting off the point - which is Swift protocols. The reality is that trying to use implementations like this is not what I wanted to do. I wanted my view controllers to be aware of the protocols representing the model data. Not the implementations because if I need to swap out or mock those implementations, I would then have to updated the interface code to match.

I've messed around with this for ages trying to find a way to be expressive about what my types are, and still get the thing to compile. The result is that effectively you can't and I ended up with this as my DetailsManagingViewController:

class DetailsManagingViewController:UIViewController {  
    var controllers = [UIViewController]()
    override func viewDidLoad() {
        controllers = [ProjectDetailsViewController()]
    }
}

As you can see I've had to remove all references to generic types and go back to using the base UIViewController. The logic being to get back to a type that isn't a generic and therefore can represent anything as such.

Protocols and types

Here's another thing. I was trying to pass a details view controller to a method without knowing what model object the view controller contained. Again in Java land I wuld simply be able to pass an instance of DetailsViewController<?> and let the runtime resolve it when running.

Swift cant do this. So I tried to use DetailsViewController<ModelObject>. This made sense because I had modified Project and ProjectImpl to look like this:

protocol Project:ModelObject {  
    var name:String? { get }
}

class ProjectImpl:Project {  
    internal var id: Int?
    typealias IdType = Int
    var name: String?
}

So theoretically, anything that accepts a ModelObject should accept a Project because it derives from ModelObject... Nope! Look at this:

let mo:ModelObject = ProjectImpl()  
error: protocol 'ModelObject' can only be used as a generic constraint because it has Self or associated type requirements  
let mo:ModelObject = ProjectImpl()  

Anyone whose tried to use associated types in protocols will have most likely come across this. Effectively, Swift is telling us that it cannot compile anything with a type of ModelObject because at compile time it doesn't know what the type of the associated type will be. ie. What type will IdType be. It appears that it cannot work it out from the value being assigned.

Associated types in protocols are a pain. Apple uses them extensively, but mostly in protocols that are compositing things like it's collection classes where you are unlikely to try and use those types as variable types.

In other cases, the only way to deal with this is to use what is known as a Thunk. Effectively a Thunk is a wrapper class for the offending protocol that takes the protocol's type as a generic argument. The idea is that the class follows the decorator pattern and implements the protocol, passing all values and function calls to the original object.

Heres the Thunk for the ModelObject protocol:

private extension ModelObject {  
    func getId() -> IdType? {
        return id
    }
    func setId(_ newId:IdType?) {
        id = newId
    }
}

class AnyModelObject<T>:ModelObject {  
    typealias IdType = T
    var id:T? {
        get {
            return idGetter()
        }
        set {
            idSetter(newValue)
        }
    }
    private var idGetter:() -> T?
    private var idSetter:(_ newId:T?) -> Void
    init<U:ModelObject>(_ object:U) where U.IdType == T {
        self.idGetter = object.getId
        self.idSetter = object.setId
    }
}

Phew! That's ugly. And yes, this is the sort of thing Apple does in their APIs too. We still can't setup variables with a type of ModelObject. But we can do this:

let mo:AnyModelObject<Int> = [AnyModelObject(ProjectImpl())]  

Thunks can sort of get around associated types because they allow us to use a type for wrapping objects that conform to the specified protocol.

But we still have to specify the generic types we are wrapping. So they're not a compete solution to our base problem of wanting to have a collection of a base type without knowing it's generic types.

So where are we?

The above code is just one approach. there are many different ways that this code can be organised to try and get around the fundamental problem. Unfortunately non of the solutions I've tried fix it. Each one addresses one area, but breaks others. None fix everything, and none address the core issue with Swift.

That is - That the decision to make everything in Swift use compile type type resolution was short sighted and fundamentally a bad idea. At best a hybrid compile/runtime resolution should have been used where the compiler recognizes it cannot deduce a type at compile type and flips that section of code to runtime resolution. I'll freely admit I'm no expert here and I'm sure that the people who designed Swift will have all sorts of explanations why this is so.

But that doesn't solve the problems with protocols.


Comments powered by Disqus