Rupert
Rupert

Reputation: 2159

Why can my simple protocol only be used as a generic constraint?

I'm trying to do some protocol composition for use in dependency injection but I'm running into a problem which I suspect may not have the solution I would like, but for which I cannot see the logical cause:

protocol DM1 {
    func sayHi() -> Void
}

protocol DM2 {
    func sayHello() -> Void
}

protocol VM1 {
    typealias T: DM1
    var dm: T { get }
}

protocol VM2 {
    typealias T: DM2
    var dm: T { get }
}

protocol RT: VM1, VM2 {

}

class James {
    let rt: RT

    init(rt: RT) {
        self.rt = rt
    }
}

The above code results in an error "Protocol 'RT' can only be used as a generic constraint because it has Self or associated type requirements" on the rt instance variable and instantiation parameter for James. I don't really understant why I can't use this generic requirement in my James class.

I had originally done something like below:

protocol DM1 {
    func sayHi() -> Void
}

protocol DM2 {
    func sayHello() -> Void
}

protocol VM1 {
    var dm: DM1 { get }
}

protocol VM2 {
    var dm: DM2 { get }
}

protocol RT: VM1, VM2 {

}

struct Fred: DM1, DM2 {
    func sayHi() {
        println("hi")
    }
    func sayHello() {
        println("hello")
    }
}

struct Bob: RT {
    let dm: Fred
}

class James {
    let rt: RT

    init(rt: RT) {
        self.rt = rt
    }
}

But this fails because "Type 'Bob' does not conform to protocol 'VM1'" (and VM2) which I can sort of understand since my protocol is requiring the variable to be of the particular protocol type rather than some instance type which conforms to that protocol. The above version was therefore meant to be a way around this.

Does anyone have a solution for what I want to do (be able to make a concrete struct which conforms to RT by having as a dm property a concrete struct that conforms to both DM1 and DM2)?

Upvotes: 3

Views: 624

Answers (1)

Aaron Rasmussen
Aaron Rasmussen

Reputation: 13316

protocol RT inherits from protocol VM1 and protocol VM2, which both have typealias requirements.

A protocol with a typealias requirement can only be used as a type restraint, not as a type.

Even a simple protocol like this...

protocol MyProtocol {
    typealias Empty
}

...(which is totally useless) can only be used as a type constraint.

There is some information at this link that might be helpful.

EDIT:

I'll do my best to explain why protocols with typealias requirements can only be used as type restraints.

Think of a protocol as a contract. This protocol promises to supply a mutable value named promise of type Int:

protocol PromiseIntType {
    var promise: Int { get set }
}

The contract that PromiseIntType makes is explicit with regard to everything that you need to know to use it as a type with regard to the promises that it makes. So if you have a class like this...

class A {
    var contract: PromiseIntType
}

...you know that if you write this...

let intValue = A().contract.promise

...that intValue will be an Int. And if you want to set the value of the type's promise property, you know that you will need to supply an Int for the new value:

let a = A()
a.contract.promise = 100

All of the terms of the contract are known in advance, and you know in advance what kinds of promises are being made and what types you are working with.

A PromiseIntType can be used exactly as though it had been defined as an actual type, like this:

struct PromiseInt {
    var promise: Int
}

Now throw a typealias requirement in the mix:

protocol PromiseSomeType {
    typealias Surprise
    var promise: Surprise { get set }
}

What promise does PromiseSomeType make? It says that it will supply a mutable value called promise, but it doesn't tell you what type that value will be. All it tells you is that whatever it provides will be a Surprise. Not all terms of the contract are known in advance. Some of the details will be filled in later.

But that makes it impossible to use PromiseSomeType as a type. Look at this class, and ask yourself what you can do with the contract property:

class B {
    var contract: PromiseSomeType
}

For example, how would you go about setting it?

let b = B()
b.contract.promise = <What type goes here?>

What type value would you get if you tried to access the promise property?

let someValue = b.contract.promise // What type is someValue?

[Additional EDIT:

How would you use someValue? If it is an Int, then you could do this:

let newValue = someValue + 12

But you have no way of knowing at compile time if someValue is an Int or not. Swift insists on knowing the type of every constant, variable, and object at compile time so that it can check to see if the operations you perform on the type are legal. If it deferred these determinations to runtime, illegal operations would crash the whole program, and we would lose the benefits that type-safety provides.

/Additional Edit]

You fill in the details of the PromiseSomeType contract when you create the actual type that fulfills the contract:

struct PromiseInt: PromiseSomeType {
    var promise: Int
}

PromiseInt says that it will fulfill the PromiseSomeType contract, and it fills in the missing details of what type the promise property will be.

Using PromiseSomeType as a type restraint might seem like it just pushes the ambiguity down the line:

class C<T: PromiseSomeType> {
    var contract: T
}

In that case, the details of the contract get filled in when create an instance of the generic type and specify the actual type that you are using:

let c = C<PromiseInt>(contract: PromiseInt(promise: 0))
c.contract.promise = 100   // You know that promise is of type Int

Either way, before you can actually use an object, all of the details of its types have to be known.

I suppose the point is that Swift is a type safe language. You can't create ambiguous types. Protocols that use a typealias are ambiguous and therefore can't be used as types, but only as type constraints.

Upvotes: 7

Related Questions