46

Why does the following code produce an error?

protocol ProtocolA {
    var someProperty: ProtocolB { get }
}

protocol ProtocolB {}
class ConformsToB: ProtocolB {}

class SomeClass: ProtocolA { // Type 'SomeClass' does not conform to protocol 'ProtocolA'
    var someProperty: ConformsToB

    init(someProperty: ConformsToB) {
        self.someProperty = someProperty
    }
}

The answer in this similar question makes sense. However, in my example, the property is get-only. Why shouldn't this work? Is it a shortcoming of Swift, or is there some reason this makes sense?

Community
  • 1
  • 1
solidcell
  • 7,639
  • 4
  • 40
  • 59

3 Answers3

60

There's no real reason why this shouldn't be possible, a read-only property requirement can be covariant, as returning a ConformsToB instance from a property typed as ProtocolB is perfectly legal.

Swift just currently doesn't support it. In order to do so, the compiler would have to generate a thunk between the protocol witness table and conforming implementation in order to perform the necessary type-conversion(s). For example, a ConformsToB instance would need to be boxed in an existential container in order to be typed as ProtocolB (and there's no way the caller can do this, as it might not know anything about the implementation being called).

But again, there's no reason why the compiler shouldn't be able to do this. There are multiple bug reports open over this, this one which is specific to read-only property requirements, and this general one, in which Slava Pestov, a member of the Swift team, says:

[...] we want protocol witnesses and method overrides in every case where a function conversion is allowed

So it definitely looks like something the Swift team are looking to implement in a future version of the language.

In the mean time however, as @BallpointBen says, one workaround is to use an associatedtype:

protocol ProtocolA {
    // allow the conforming type to satisfy this with a concrete type
    // that conforms to ProtocolB.
    associatedtype SomeProperty : ProtocolB
    var someProperty: SomeProperty { get }
}

protocol ProtocolB {}
class ConformsToB: ProtocolB {}

class SomeClass: ProtocolA {

    // implicitly satisfy the associatedtype with ConformsToB.
    var someProperty: ConformsToB

    init(someProperty: ConformsToB) {
        self.someProperty = someProperty
    }
}

But this is quite unsatisfactory, as it means that ProtocolA is no longer usable as a type (because it has associatedtype requirements). It also changes what the protocol says. Originally it said that someProperty could return anything that conformed to ProtocolB – now it says that an implementation of someProperty deals with just one specific concrete type that conforms to ProtocolB.

Another workaround is just to define a dummy property in order to satisfy the protocol requirement:

protocol ProtocolA {
    var someProperty: ProtocolB { get }
}

protocol ProtocolB {}
class ConformsToB: ProtocolB {}

class SomeClass: ProtocolA {

    // dummy property to satisfy protocol conformance.
    var someProperty: ProtocolB {
        return actualSomeProperty
    }

    // the *actual* implementation of someProperty.
    var actualSomeProperty: ConformsToB

    init(someProperty: ConformsToB) {
        self.actualSomeProperty = someProperty
    }
}

Here we're essentially writing the thunk for the compiler – but it's also not particularly nice as it adds a needless property to the API.

Hamish
  • 78,605
  • 19
  • 187
  • 280
  • 1
    Thanks for the detailed answer @Hamish. I've already been doing what you've suggested (the computed property wrapper), but I agree, it's unfortunate to have to add another property. – solidcell Mar 10 '17 at 12:34
  • There is at least 1 potential problem with covariance of read-only protocol properties. Here: https://forums.swift.org/t/should-allow-covariance-of-get-only-protocol-property-requirements/27937/10 – Anton Belousov Jun 23 '20 at 22:34
  • The youtube link provided in this answer is down due to copyright issues! – Sourav Kannantha B Feb 09 '21 at 05:08
2

In addition to Harmish's great response, if you want to keep using the same property name on both SomeClass and ProtocolA, you can do

protocol ProtocolB {}

protocol ProtocolA {
    var _someProperty_protocolA: ProtocolB { get }
}

extension ProtocolA {
    var someProperty: ProtocolB {
        return _someProperty_protocolA
    }
}

class ConformsToB: ProtocolB {}

class SomeClass: ProtocolA {


    // the *actual* implementation of someProperty.
    var _someProperty: ConformsToB

    var someProperty: ConformsToB {
      // You can't expose someProperty directly as
      // (SomeClass() as ProtocolA).someProperty would
      // point to the getter in ProtocolA and loop
      return _someProperty
    }

    // dummy property to satisfy protocol conformance.
    var _someProperty_protocolA: ProtocolB {
        return someProperty
    }

    init(someProperty: ConformsToB) {
        self.someProperty = someProperty
    }
}

let foo = SomeClass(someProperty: ConformsToB())
// foo.someProperty is a ConformsToB
// (foo as ProtocolA).someProperty is a ProtocolB

This can be useful when you are conforming to another protocol ProtocolA2 that would originally also have constraint on someProperty as well, or when you want to hide your hack around swift limitations.

I'm now curious to know why Swift is not doing this for me directly.

Guig
  • 9,891
  • 7
  • 64
  • 126
1

Beginning in Swift 5.1, you can use opaque return types to reference a protocol that references another protocol, so long as you also use associatedtypes to do so.

Not only does it work for readonly "get" properties, but also readwrite properties. For example,


protocol ProtocolA {
  associatedtype T: ProtocolB
  var someProperty: T { get }
  var x: Int { get set }
}

protocol ProtocolB {
  var x: Int { get set }
}

struct ConformsToB: ProtocolB {
  var x: Int
}

class SomeClass: ProtocolA {
  var someProperty: ConformsToB

  init(someProperty: ConformsToB) {
    self.someProperty = someProperty
  }

  var x: Int {
    get {
      someProperty.x
    }
    set {
      someProperty.x = newValue
    }
  }
}

var protocolA: some ProtocolA = SomeClass(someProperty: ConformsToB(x: 1))

print(protocolA.x) // 1
protocolA.x = 2
print(protocolA.x) // 2
Ilias Karim
  • 4,798
  • 3
  • 38
  • 60