-
-
Save brentsimmons/7ec7e3669ff7ad17e446 to your computer and use it in GitHub Desktop.
//: Playground - noun: a place where people can play | |
import Cocoa | |
protocol Account: Equatable { | |
var accountID: String {get} | |
} | |
// https://twitter.com/optshiftk/status/628985834801336320 | |
func ==<T: Account>(lhs: T, rhs: T) -> Bool { | |
return lhs.accountID == rhs.accountID | |
} | |
class FooAccount: Account { | |
let accountID = "foo" | |
} | |
class BarAccount: Account { | |
let accountID = "bar" | |
} | |
let foo = FooAccount() | |
let bar = BarAccount() | |
var accounts = [Account]() | |
// Error on line 29: protocol 'Account' can only be used as a generic constraint because it has Self or associated type requirements |
I'm still tinkering with using Accounts in Collections but this might help
protocol Account {
var accountID: String {get}
}
extension Account where Self : Equatable {}
func ==(lhs: Account, rhs: Account) -> Bool {
return lhs.accountID == rhs.accountID
}
It's not pretty, but this'll do the trick:
struct AnyAccount: Account {
var accountID: String {return accountIDClosure()}
private let accountIDClosure: () -> String
init<T: Account>(_ base: T) {
accountIDClosure = {base.accountID}
}
}
let foo = FooAccount()
let bar = BarAccount()
var accounts: [AnyAccount] = [AnyAccount(foo), AnyAccount(bar)]
accounts.first!.accountID
accounts.contains(AnyAccount(foo))
Apple seems to do similar wrapping with AnySequence
because you can't return a SequenceType
directly.
The issue here is that Swift does not allow you to use a protocol as a type parameter when said protocol has a Self or associated type requirement. [Account]
desugars to Array<Account>
, and Account
conforms to Equatable
, whose definition contains a Self type:
func ==(_ lhs: Self, _ rhs: Self) -> Bool
Currently in Swift there is no way use type bounds as part of a variable's type annotation. You can't say, for instance, var accounts: Array<Any: Account>
, where Any: Account
denotes "any type that conforms to the Account
protocol". Normally, Any: Account
(which is only a theoretical construct in Swift here) and just Account
mean exactly the same thing. These two behave the same way:
func accountIdOf(account: Account) -> String {
return account.accountId
}
func accountIdOf<T: Account>(account: T) -> String {
return account.accountId
}
But this sort of generic notion is not possible on the level of a variable's type annotation. In languages with similar functionality, this notion of variable-level type bounds is possible. For example, Scala has either val accounts: Seq[_ <: Account]
or val accounts: Seq[T] forSome { type T <: Account }
and calls it an existential type with a type bound, whereas Java allows List<? extends Account> accounts
and calls it bounded wildcards.
Further adding to this mess is that Swift currently offers no control over variance. Suppose you have a type B
that is a subtype of A
. Then wherever you have a let x: A
you may assign to it a value of type B
. But more interestingly [B]
is considered a subtype of [A]
and B?
a subtype of A?
because Arrays and Optionals are magically covariant in Swift. If you were to define your own container type, like a class Box<T>
, then there is no way for you to tell the compiler that you consider Box<B>
to be a valid subtype of Box<A>
. C#, for example, lets you say class Box<out T>
to mark the type parameter as covariant.
Objective-C does not suffer this problem on two different levels. For one, it supports __covariant
and __contravariant
, so in terms of variance support it is ahead of Swift. In this particular instance, [- NSArray containsObject:]
is simply not concerned with whether an object is capable of proper equality checks. All derived classes of NSObject
inherit an identity comparison implementation of [- NSObject isEqual:]
anyway, and it is up to the subclass's responsibility to override it with something meaningful, so other than disciplined vigilance, there's no compile-time guarantee that [- NSArray containsObject:]
will do the right thing.
My current best guess as to why a protocol as a type parameter is not allowed under certain circumstances, is that variance can interact with associated / self type constraints in surprising manners. All in all, this inability to hold an simple array of things that are only common by protocol conformance is indicative that Swift's type system still has many refinements that can be made.
import Cocoa
protocol AnyEquatable {
func isEqual(other: Any) -> Bool
}
protocol Account: AnyEquatable {
var accountID: String {get}
}
extension SequenceType where Generator.Element : AnyEquatable {
func containsAny(element: Any) -> Bool {
for e in self {
if e.isEqual(element) {
return true
}
}
return false
}
}
func containsAccount(accounts: [Account], element: Account) -> Bool {
for e in accounts {
if e.isEqual(element) {
return true
}
}
return false
}
func containsT<T: AnyEquatable>(array: [T], element: T) -> Bool {
for e in array {
if e.isEqual(element) {
return true
}
}
return false
}
class FooAccount: Account {
let accountID = "foo"
func isEqual(other: Any) -> Bool {
guard let _ = other as? FooAccount else {
return false
}
return true // TODO
}
}
class BarAccount: Account {
let accountID = "bar"
func isEqual(other: Any) -> Bool {
guard let _ = other as? BarAccount else {
return false
}
return true // TODO
}
}
let foo = FooAccount()
let bar = BarAccount()
var accounts: [Account] = [foo, bar]
containsAccount(accounts, element: foo) // works
containsT(accounts, element: foo) // error: Cannot convert value of type '[Account]' to expected argument type '[_]'
accounts.containsAny(foo) // error: Using 'Account' as a concrete type conformint to protocol 'AnyEquatable' is not supported
protocol Account {
var accountID: String { get }
}
extension SequenceType where Self.Generator.Element == Account {
func contains(element: Self.Generator.Element) -> Bool {
for currentElement in self {
if currentElement.accountID == element.accountID {
return true
}
}
return false
}
}
class FooAccount: Account {
let accountID = "foo"
}
class BarAccount: Account {
let accountID = "bar"
}
let foo = FooAccount()
let bar = BarAccount()
var accounts = [Account]()
if !accounts.contains(foo) { // works
accounts.append(foo)
}
if !accounts.contains(bar) { // works
accounts.append(bar)
}
How about this?
import Cocoa
protocol Feed {
var url: String {get}
func isEqualTo(other: Feed) -> Bool
}
extension Feed where Self: Equatable {
func isEqualTo(other: Feed) -> Bool {
return url == other.url
}
}
func ==<T: Feed where T:Equatable> (lhs: T, rhs: T) -> Bool {
return lhs.isEqualTo(rhs)
}
protocol Folder {
var feeds: [Feed] {get}
func addFeeds(feedsToAdd: [Feed])
}
// This could be done if the Folder feeds had a { get set }
//extension Folder {
// func addFeeds(feedsToAdd: [Feed]) {
// for oneFeed in feedsToAdd {
// if !feeds.contains({ $0.isEqualTo (oneFeed) }) {
// feeds += [oneFeed]
// }
// }
// }
//}
class LocalFeed: Feed {
var url: String
init(url: String) {
self.url = url
}
}
extension LocalFeed: Equatable {}
class LocalFolder: Folder {
var feeds = [Feed]()
func addFeeds(feedsToAdd: [Feed]) {
for oneFeed in feedsToAdd {
if !feeds.contains({ $0.isEqualTo (oneFeed) }) {
feeds += [oneFeed]
}
}
}
}
FTA: "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 realize that protocols are the new hotness, but it appears that what you're wanting to do is write a protocol Account and then do a protocol extension to add Account functionality to a TwitterAccount object.
All well and good, but there could be namespace issues when adding extension methods to an unknown classes.
Seems to me that doing an abstract Account base class and then an TwitterAccountFacade:Account that manages the Twitter account object would be the better approach. Not only can you then add your own functions without worrying about possible collisions, but the facade that owns the TwitterAccount object can also better manage the object's lifecycle and hide the machinations of actually creating a TwitterAccount object from the rest of the app.
I mean, when you get right down to it you're either going to be writing a TwitterAccountExtension or a TwitterAccountFacade with fundamentally the same methods and code, which pretty much means you're in the six-of-one vs. half-dozen-of-the-other category.
Minus the hotness, of course.