Problems with Protocol-Oriented Design

Hi,


I watched the WWDC talk on Protocol-Oriented Design, but it didn't really address the many problems I've had dealing with protocols in Swift. They mostly relate to generic protocols, which I was surprised to see barely touched on at all.


You can't store generic protocols


Let's try something like this:


struct MergedCollection<T>
{
     var data : [CollectionType] //where CollectionType.Generator.Element==T
     init<C:CollectionType where C.Generator.Element==T>( collections: [C] )
     {
          data = collections
     }
}


If you try this code, you'll get an error because you can't just store something in terms of CollectionType. You can store an Array<T>, a Set<T>, or some other concrete implementation of CollectionType, but you can't store something typed to a generic protocol. It isn't possible to write data structures which store and operate only in terms of protocols. You very quickly need to jump down to concrete types (structs, classes).


Note: You could technically store these by downcasting to Any, but since you can't cast to- or switch against generic protocols, you can't get any data out of them ever again.


Even if you could store the collections in terms of CollectionType (and you can kind of hack it with a block-based wrapper in the style of GeneratorOf<T>), notice that we've had to constrain C.Generator.Element to be equal to T. That brings me on to the next pain point.


No conformance specifier for generic type parameters (covariance)


I have a protocol which represents a datasource for a flat collection of items. I've made it conform to CollectionType, with a couple of additions:


protocol Identifiable
{
     var uniqueIdentifier : String { get }
}

protocol Library : CollectionType
{
     typealias Item : Identifiable

     subscript( position: Int ) -> Item { get }
     subscript( uniqueIdentifier: String ) -> Item? { get }
}


There are a few different types of Library in my application - some which load their data from the filesystem, others which load their information from the internet and may change their content. There are also different types of items they might store:


protocol ContentManager {}
struct RemoteContentManager  : ContentManager {}
struct BundledContentManager : ContentManager {}

protocol Book : Identifiable
{
     var contentManager : ContentManager { get }
}

struct RemoteBook : Book
{
     var uniqueIdenfier = "demo"
     var contentManager = RemoteContentManager()

     // Code unique to RemoteBook
}

struct BundledBook : Book
{
     var uniqueIdenfier = "demo"
     var contentManager = BundledContentManager()

     // Code unique to BundledBook
}


This won't work. You'll need to expliticly upcast the content managers because you're losing type information. Unless you want Book to become a generic protocol (and you don't, because then you can basically do nothing with it as a means of abstraction in your code), so you should just do that: ( RemoteContentManager() as ContentManager )


To make dealing with these different Librarys with their many kinds of Book easier, and to present a simpler abstraction for my interface components, I want to create a "MergedLibrary" collection type. Ideally, I'd create a "MergedLibrary<Book>", add a few different Librarys to it whose items all conform to Book, and present that neatly to my interface components.


If we made Book a generic protocol, we couldn't do this. We couldn't create a MergedLibrary<Book> or an Array<Book> or anything like that. We'd need to work in concrete subtypes of Book and not in protocols.


class RemoteItemsLibrary : Library
{
     var books : [RemoteBook]
     subscript( position: Int ) -> RemoteBook { return books[ position] }
     subscript( uniqueIdentifier: String ) -> RemoteBook? { books.indexOf{ $0.uniqueIdentifier == uniqueIdentifier }.flatMap{ books[ $0 ] } }        

//   Code to populate books property from internet
}

class BundledItemsLibrary : Library
{
     var books : [BundledBook]
     subscript( position: Int ) -> BundledBook { return books[ position] }
     subscript( uniqueIdentifier: String ) -> BundledBook? { books.indexOf{ $0.uniqueIdentifier == uniqueIdentifier }.flatMap{ books[ $0 ] } }

//   Code to populate books property from filesystem
}

class MergedLibrary<T:Identifiable>
{
     init<L:Library where L.Item:T>( libraries: L... ) // ERROR: L.Item constrained to non-protocol type 'T'
     {
          ...
     }
}

let remoteBooksLibrary  : RemoteItemsLibrary  = getRemoteItemsLibrary()
let bundledBooksLibrary : BundledItemsLibrary = loadBundledItems()

let mergedLibrary : MergedLibrary<Book> = MergedLibrary( libraries: remoteBooksLibrary, bundledBooksLibrary )


Now, to make MergedLibrary accept both RemoteBook and LocalBook, we need to allow accept all libraries with any kind of Book. That isn't possible because "T" isn't a protocol. In this case, T does actually happes to be a protocol (and a non-generic one at that!), but whatever.


The way to get around this is to remove the generic type parameter -- we can, in fact, constrain L.Item to a specific type (such as UIView, Book, or whatever).

So that means we need to un-genericise this in to a MergedBooksLibrary which concretely defines T as Book, MergedSongLibrary for Song, MergedMovieLibrary for Movie, etc.


Basically, Swift is just not ready for Protocol-oriented design. You'll get quite far with it, then it'll just explode suddenly once you hit a critical missing piece of implementation or design question. For instance, you'll need to add a typealias or Self surprisingly often, your protocol will become generic, and most of your architecture just won't be possible any more even when used by generic types. This isn't one of the things which has improved much with Swift 2.

So maybe I don't get what you are saying with your first example.


struct MergedCollection<T where T : CollectionType>
{
    var data : [T]
    init(collections: [T]) {
        data = collections
    }
}

let items1 = [1, 2, 3]
let items2 = [4, 5, 6]
let c = MergedCollection<[Int]>(collections: [items1, items2])


Isn't this what you are tasking for? The ability to store multiple collections that hold a type of T that implements CollectionType?


Also, if you need a library to accept any type of book, then right, the protocol cannot be generic because the type information is important. Instead, do something like this:


struct ContentManager<T> {
}
protocol AnyBook {
    var identifier: String { get }
    init(identifier: String)
}
protocol Book : AnyBook {
    var contentManager: ContentManager<Self> { get }
}
struct LocalBook : Book {
    let identifier: String
    let contentManager = ContentManager<LocalBook>()
   
    init(identifier: String) {
        self.identifier = identifier
    }
}
struct RemoteBook : Book {
    let identifier: String
    let contentManager = ContentManager<RemoteBook>()
   
    init(identifier: String) {
        self.identifier = identifier
    }
}
let items: [AnyBook] = [LocalBook(identifier: "local"), RemoteBook(identifier: "remote")]


If you actually needed to use the generic information from the ContentManager, for example, you'd need to be working with specific types anyway.


It sounds like you are trying to create a situation where both the type matters and doesn't matter. Obviously, that's not going to work in Swift. But you can tier your types into sections where the information that is type agnostics can go in a lower protocol.


Maybe I missed something?

In the first example, T itself isn't a type of CollectionType.


The closest you could get to implementing a data structure which operates solely in terms of CollectionType is to do something like this:


struct MergedCollection<T,C : CollectionType where C.Generator.Element == T>
{
  var data : [C]
  init( collections: [C] )
  {
      data = collections
  }
}
let collectionOne = [1,2,3]
let collectionTwo = [4,5,6]
let merged = MergedCollection( collections: [collectionOne, collectionTwo] )


The problem with this, is that we still aren't working solely in terms of the CollectionType protocol; we're working in terms of a single derived type of that protocol -- in this case, we don't have MergedCollection<Int> as desired, but MergedCollection<Int, Array<Int>>.


This is just to illustrate the point that there is basically nothing you can do with generic protocols as a means of abstraction in your program.


If this was a non-generic protocol, I could accept an array of mixed types which all conform, store them and use them with no more type information. That's a useful abstraction mechanism - I don't need to care what the type is, only that it is, for example, CustomStringConvertible. An Array<CustomStringConvertible> can store Ints, Doubles, Bools, and whatever else you like that conforms.


Once it becomes a generic protocol, you can only use the protocol as a generic type parameter. You can't store it (except by completely erasing type information), and even if you manage to you can't get it out again because there's no way to query type information or cast to generic protocols. You have to bind the associated types to additional generic type parameters (T in this case), which then may not be protocols and have no notion of covariance in this case (it does have the notion of covariance for concrete types; I can write a type qualifier "C.Generator.Element : UIView", for instance).


Basically, non-generic protocols are nice and flexible, but once you add even a single associated type your entire program architecture dies in a puff of smoke and you need to revert to making things more rigidly linked.


As for the second example, ContentManager can't/shouldn't be generic. BundledContentManager and RemoteContentManager have totally different implementations but a common interface; surely this is what protocols are for?

I'm not 100% sure what you want out of your MergedCollection, but from what I can understand of your description it seems like you should look at the type-erased containers like SequenceOf<T> for inspiration.

let a: Array = [1,2,3]
let s: Set   = [1,2,3]

let combined: [SequenceOf<Int>] = [SequenceOf(a), SequenceOf(s)]

FYI, It's been renamed AnySequence in Swift 2. And there are some other Any___ type erased wrappers.

Ah, thanks. Playgrounds don't work at all in my Xcode 7 beta install (no matter what I do they just spin continuously) so I haven't had the chance to play around with Swift 2 much.

Problems with Protocol-Oriented Design
 
 
Q