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.