Skip to content

[Docs] Rework educational note on protocol type non-conformance #33735

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Sep 1, 2020
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 44 additions & 29 deletions userdocs/diagnostics/protocol-type-non-conformance.md
Original file line number Diff line number Diff line change
@@ -1,46 +1,61 @@
# Protocol type not conforming to itself
Protocols in Swift may be used as types. Protocols as types are sometimes called existential types.
# Protocol Types Cannot Conform to Protocols

In Swift, a protocol that does not have `Self` or associated type requirements can be used as a type. You can use a variable or constant of a protocol type, also called an __existential type__, to hold a value of any conforming type:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be “contravariant Self”, because this works:

protocol P {
  func foo() -> Self
}

struct S: P { 
  func foo() -> S {
    self
  }
}

let p: P = S() // okay

Copy link
Collaborator Author

@xwu xwu Sep 1, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The details are more applicable to the other educational note on PATs. However, I'm a bit surprised that this works, since var foo: Self { get } does not.

Edit: I've opened a draft update of the other docs, where I think the details are most salient for what you point out. Still not sure why the discrepancy between func and var here though...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if I'm thinking on the right track, just a thought to throw about:

One of the differences between functions and computed variables is that only functions can throw errors. Error is the only protocol in Swift that has self-conformance. Is there any special handling added to functions because of Error, that might explain the discrepancy?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not just Error. Any and many Obj-C protocols are also self-conformant.


```swift
protocol P {}

struct S: P {}
protocol Animal {
func makeNoise()
static var species: String { get }
}
struct Dog: Animal {
func makeNoise() { print("Woof") }
static var species: String = "Canus familiaris"
}
struct Cat: Animal {
func makeNoise() { print("Meow") }
static var species: String = "Felis catus"
}

var s: P = S() // This creates existential type because the protocol P is used as a type
var animal: Animal // `Animal` is used here as a type.
animal = Dog()
animal.makeNoise() // Prints "Woof".
animal = Cat()
animal.makeNoise() // Prints "Meow".
```

However, a protocol type does not conform to protocols - not even the protocol itself.
Allowing existential types to conform to protocols is unsound. For protocols with static method, initializer, or associated type requirements, the implementation of these requirements cannot be accessed from the protocol type - accessing these kinds of requirements must be done using a concrete type.

Let's walk through the example below:
Notice that it is possible to invoke the method `makeNoise()` on a value of type `Animal`, just as it is possible to do so on a value of concrete type `Dog` or `Cat`. However, the static property `species` is not available for the existential type:

```swift
protocol Word: Hashable {
var word: String { get }
}
print(Dog.species) // Prints "Canus familiaris"
print(Cat.species) // Prints "Felis catus"
print(Animal.species) // error: static member 'species' cannot be used...
```

struct Singular: Word {
var word: String
}
Since a type conforms to a protocol only when it satisfies _all_ of that protocol's requirements, the existential type `Animal` does not conform to the protocol `Animal` because it cannot satisfy the protocol's requirement for the static property `species`:

struct Plural: Word {
var word: String
```swift
func declareAnimalSpecies<T: Animal>(_ animal: T) {
animal.makeNoise()
print("My species is known as \(T.species)")
}

let singularWord = Singular(word: "mango")
let pluralWord = Plural(word: "mangoes")

let wordPairDict: [Word: Word] = [singularWord: pluralWord] // Error
let dog = Dog()
declareAnimalSpecies(dog)
// Prints:
// "Woof"
// "My species is known as Canus familiaris"
declareAnimalSpecies(animal)
// error: protocol type 'Animal' cannot conform to 'Animal'...
```

One workaround to fix this problem is to use type erasure for the protocol `Word`. Think of type erasure as a way to hide an object's type. Since `Word` is of type `Hashable`, we already have `AnyHashable` type erasure available in the standard library which we can easily use here.
In general, any initializers, static members, and associated types required by a protocol can be used only via conforming concrete types. Although Swift allows a protocol that requires initializers or static members to be used as a type, that type _does not and cannot_ conform to the protocol itself.

```swift
// The fix
let wordPairDict: [AnyHashable: AnyHashable] = [singularWord: pluralWord]
```
Currently, even if a protocol `P` requires no initializers or static members, the existential type `P` does not conform to `P` (with exceptions below). This restriction allows library authors to add such requirements (initializers or static members) to an existing protocol without breaking their users' source code.

Concrete types that _do_ conform to protocols can provide functionality similar to that of existential types. For example, the standard library provides the `AnyHashable` type for `Hashable` values. Manual implementation of such __type erasure__ can require specific knowledge of the semantic requirements for each protocol involved and is beyond the scope of this discussion.

For more on using existential types, see [Protocols as Types](https://docs.swift.org/swift-book/LanguageGuide/Protocols.html#ID275) in _The Swift Programming Language_.

# Exceptions
`@objc` protocol type with no static requirements however do conform to its own protocol. Another exception is the `Error` Swift protocol.
## Exceptions

The Swift protocol `Error` has no required members and, when used as a type, conforms to itself; `@objc` protocols with no static requirements can also be used as types that conform to themselves.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any is also self-conforming, though it's not technically a protocol.

Two other notes about self-conforming protocols that may be relevant:

  1. By definition, self-conforming existential types satisfy the condition P.self is P.Type. (Although Swift's current casting implementation has bugs around this.)

  2. Self-conformance is necessary for certain generic constructs in Swift to work. You gave a basic example for generic functions, but it also applies to generic types. It's worth talking about workarounds in this document, as this seems to be the primary reason that people run into this diagnostic: They're trying to do perfectly reasonably things with generics that Swift in its current form does not support.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tbkka Thanks!

Regarding (1), are the bugs sufficiently rare that we can tell users that this relationship is true of today's Swift?

Regarding (2), can you help outline what you're referring to in terms of its applicability to generic types? Besides manual type erasure, what other workarounds are you thinking of in that case?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Unfortunately, Swift 5.3 only handles this correctly for Any

  2. Besides manual type erasure, you can avoid self-conformance problems by just using protocol-typed arguments and ivars directly, instead of making them generic:

// This has problems with protocol-typed arguments:
func f<T:P>(t:T) { ... }
// Instead, just use the protocol-typed argument directly:
func f(t: P) { ... }

// Similarly for generic types.
// Instead of:
struct Foo<T:P> {
    let t: T
    ....
}
// Just use a protocol-typed ivar directly and avoid the generic
struct Foo {
   let t: P
   ...
}

Because of how Swift handles protocols and generics, the latter is essentially the same but avoids the self-conformance problem.