inessential by Brent Simmons

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.