Skip to content

Instantly share code, notes, and snippets.

@brentsimmons
Created August 5, 2015 18:07
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save brentsimmons/7ec7e3669ff7ad17e446 to your computer and use it in GitHub Desktop.
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
@hmlongco
Copy link

hmlongco commented Aug 7, 2015

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.

@wildthink
Copy link

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
}

@nschum
Copy link

nschum commented Aug 14, 2015

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.

@kourge
Copy link

kourge commented Dec 16, 2015

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.

@jazzbox
Copy link

jazzbox commented Dec 16, 2015

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

@freedom27
Copy link

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)
}

@wildthink
Copy link

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]
            }
        }
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment