Swift Diary #9: Where I’m Stuck
Let’s say I’m writing a multi-platform social networking client. I know a person, and I have it on strong authority that Jack Dorsey’s going to re-open the Twitter APIs. (I can say this out loud because it sounds like I’m just pretending.) So my social networking client will support Twitter, App.net, LinkedIn, and a handful of other systems. (Let’s say.)
I’ve got the problem that I’ve written about before — protocols and collections don’t mix well.
I’m using a protocol for the Account type. There are classes that conform to the Account protocol: TwitterAccount, AppNetAccount, LinkedInAccount, etc.
There’s also an AccountManager class that manages the accounts list. The list is a Set<Account>
, as it should be (since no account should appear more than once; since ordering isn’t important).
Except, of course, no it isn’t, because Account is not Hashable.
In reality it’s an array — [Account]
— that holds all the accounts. That’s fine. That works.
Except when it doesn’t: except when I want to see if the array contains an account object. Error: cannot invoke 'contains' with an argument list of type '(Account)'
So I can’t do manually what a Set would do automatically, which is to ensure that a given account appears only once.
And, here’s the thing: this scenario doesn’t appear once but a bunch of times. (A major example is the TimelineItem protocol. Each different type of account has one or more different TimelineItem-conforming classes, and a given timeline can contain multiple different concrete TimelineItem objects.)
I’ve worked around some of these, and I’m sure I can come up with work-arounds for each case. But it’s getting increasingly frustrating. It’s slowing me down.
I have some options:
-
Continue with the work-arounds, despite the frustration. (Even though these projects are supposed to be fun.)
-
Switch to Swift plus Objective-C types — that is, make Account and TimelineObject @objc protocols and make the classes that conform to those protocols @objc types. Then use Foundation collection types NSSet and NSArray. (This is like writing Objective-C with Swift syntax.)
-
Switch to Objective-C, which handles my design perfectly. But is not Swift.
I don’t know what to do. What would you do?
* * *
Update 10:40 am: Some people are asking why Account can’t be Hashable. It’s because it can’t be Equatable. (For starters. There might be other reasons too.)
protocol Account: Equatable {
var accountID: String {get}
}
func ==(lhs: Account, rhs: Account) -> Bool {
return lhs.accountID == rhs.accountID
}
Error: protocol 'Account' can only be used as a generic constraint because it has Self or associated type requirements
.
If there’s an answer to this, then I’d be extremely keen to know what it is!
* * *
Update 11:00 am: Got Equatable working, thanks to Kyle Sluder, but not Hashable. Here’s my current gist.
* * *
Update 11:10 am: I spoke too quickly. Equatable is still the problem. Here’s the gist.
Which means I’m faced with the original point of this post. What to do?
* * *
Update: 11:35 am: Several people have suggested using a base class instead of a protocol. That should do the trick, yes.
But there were reasons to use protocols in the first place:
-
I dislike class inheritance in general.
-
I don’t like using a base class purely to satisfy the type system.
-
Requiring a base class makes doing a plug-in API more difficult. (It’s simpler to have plug-ins conform to a protocol. Though I have to admit I haven’t done any research with Swift plug-ins, so it’s possible I’m completely optimistic here anyway.)
I’m pragmatic, though, and I may end up doing this as a work-around.