Restrictions Around Protocols and Generics in Swift

Restrictions around Protocols and Generics in Swift

Your questions, 1 and 2, are in fact strongly related.

Before getting started though, I should point out that when you have first-class functions, the observable/observer pattern is almost entirely redundant. Instead of mandating interfaces for callbacks, you can just supply a closure. I’ll show this in the answer to question 2.

First, 1. The problem you are experiencing is type erasure. Your base class is the only place in which you have defined registerObserver and it looks like this:

class GenericObservable<EventType> {
private var observers:[GenericObserver] = []
func registerObserver(observer:GenericObserver) -> Bool {
// Code to avoid registering the same observer twice
observers.append(observer)
return true
}
//...
}

That is, it will take and store a protocol reference to any type. There is no constraint on what that type is, it can be anything. For example, you could notify an Int:

extension Int: GenericObserver {
func update<EventType>(observable:GenericObservable<EventType>, event:EventType) {
print("Integer \(self)")
}
}

numberObservable.registerObserver(2)

The problem will come when the callees try to use EventType. EventType could be anything. It is similar to this function:

func f<T>(t: T) { }

T can be any type you like – a String, an Int, a Foo. But you will not be able to do anything with it, because it provides zero guarantees. To make a generic type useful, you have to either constrain it (i.e. guarantee it has certain features, like that it’s an IntegerType that can be added/subtracted for example), or pass it on to another generic function that similarly unconstrained (such as put it in a generic collection, or call print or unsafeBitCast which will operate on any type).

Basically, your observers have all declared “I have a family of methods, update, which you can call with any type you like”. This is not very useful, unless you’re writing something like map or a generic collection like an array, in which case you don’t care what T is.

This might help clear up some confusion – this does not do what you think it does:

class DataObserver : GenericObserver {
func update<NSData>(observable:GenericObservable<NSData>, event:NSData) {
print("Data Event \(event)")
}
}

Here you have not declared that DataObserver specifically takes an NSData class. You’ve just named the generic placeholder NSData. Similar to naming a variable NSData – it doesn’t mean that is what the variable is, just that’s what you’ve called it. You could have written this:

class DataObserver : GenericObserver {
func update<Bork>(observable:GenericObservable<Bork>, event: Bork) {
print("Data Event \(event)")
}
}

Ok so how to implement an observable protocol with an associated type (i.e. a typealias in the protocol). Here’s an example of one. But note, there is no Observer protocol. Instead, Observable will take any function that receives the appropriate event type.

protocol Observable {
typealias EventType
func register(f: EventType->())
}
// No need for an "Observer" protocol

Now, let’s implement this, fixing EventType to be an Int:

struct FiresIntEvents {
var observers: [Int->()] = []

// note, this sets the EventType typealias
// implicitly via the types of the argument
mutating func register(f: Int->()) {
observers.append(f)
}

func notifyObservers(i: Int) {
for f in observers {
f(i)
}
}
}

var observable = FiresIntEvents()

Now, if we want to observe via a class, we can:

class IntReceiverClass {
func receiveInt(i: Int) {
print("Class received \(i)")
}
}

let intReceiver = IntReceiverClass()
// hook up the observing class to observe
observable.register(intReceiver.receiveInt)

But we can also register arbitrary functions:

observable.register { print("Unowned closure received \($0)") }

Or register two different functions on the same receiver:

extension IntReceiverClass {
func recieveIntAgain(i: Int) {
print("Class recevied \(i) slightly differently")
}
}

observable.register(intReceiver.recieveIntAgain)

Now, when you fire the events:

observable.notifyObservers(42)

you get the following output:

Class received 42
Unowned closure received 42
Class recevied 42 slightly differently

But with this technique, if you try to register a function of the wrong event type, you get a compilation error:

observable.register(IntReceiverClass.receiveString)
// error: cannot invoke 'register' with an argument list of type '(IntReceiverClass -> (String) -> ())

Swift Protocols: can I restrict associated types?

Those aren't restrictions, they're just aliases, so you can express them as aliases:

protocol SpaceInterpolatorProtocol {
associatedtype Vertex: SpaceVertexProtocol
typealias Spot = Vertex.Spot
typealias Axis = Spot.Axis
}

Unrelated code review, ignore if you like:
This does not look like a good use of protocols, and feels likely to cause a lot of problems and excessive type-erasure, but aligning the types is no issue. I would probably replace all of these with concrete structs, and look for places code duplicates, but that's unrelated to the question. The simplification of the associatedtypes down to a single type suggests the top level struct should be SpaceInterpolator<CoordUnit: FloatingPoint>.

Why don't associated types for protocols use generic type syntax in Swift?

This has been covered a few times on the devlist. The basic answer is that associated types are more flexible than type parameters. While you have a specific case here of one type parameter, it is quite possible to have several. For instance, Collections have an Element type, but also an Index type and a Generator type. If you specialized them entirely with type parameterization, you'd have to talk about things like Array<String, Int, Generator<String>> or the like. (This would allow me to create arrays that were subscripted by something other than Int, which could be considered a feature, but also adds a lot of complexity.)

It's possible to skip all that (Java does), but then you have fewer ways that you can constrain your types. Java in fact is pretty limited in how it can constrain types. You can't have an arbitrary indexing type on your collections in Java. Scala extends the Java type system with associated types just like Swift. Associated types have been incredibly powerful in Scala. They are also a regular source of confusion and hair-tearing.

Whether this extra power is worth it is a completely different question, and only time will tell. But associated types definitely are more powerful than simple type parameterization.

Is it possible to add type constraints to a Swift protocol conformance extension?

EDIT: As noted in the updated question, this is now possible since Swift 4.1


This is not currently possible in Swift (as of Xcode 7.1). As the error indicates, you can't restrict protocol conformance ("inheritance clause") to a type-constrained extension. Maybe someday. I don't believe there's any deep reason for this to be impossible, but it's currently not implemented.

The closest you can get is to create a wrapper type such as:

struct FriendlyArray<Element: Friendly>: Friendly {
let array: [Element]
init(_ array: [Element]) {
self.array = array
}
func sayHi() {
for elem in array {
elem.sayHi()
}
}
}

let friendly: Friendly = FriendlyArray(["Foo", "Bar"])

(You would likely want to extend FriendlyArray to be a CollectionType.)

For a tale of my own descent into the madness of trying to make this work, and my crawl back from the edge, see NSData, My Old Friend.



Related Topics



Leave a reply



Submit