Protocol extensions and inheritance

Under the protocols section in the language guide, it is explained that:

Protocol extensions can add implementations to conforming types but can’t make a protocol extend or inherit from another protocol. Protocol inheritance is always specified in the protocol declaration itself.

I was wondering why that is (see my emphasis). Although only a beginner, the idea of extending protocols to make them conform to other protocols seems very intuitive to me, and I would expect it to follow the behavior of types where it is allowed.

Eg, it is not directly clear to me what I should do when I don't have access to the declaration of the protocol A, but would like for all types that conform to it to also inherit conformance to another protocol B.

I'm sure I'm missing something. Looking forward to learning more!

2 Likes

Wouldn't using an extension to provide default implementation solve this caveat?

This still doesn't quite solve the problem I described, ie getting all types (not necessarily mine) that already conform to A, to also conform to B (by providing default implementation).

Here's a short example that I hope will help illustrate my point of confusion.

Let's say we have

protocol A {
    var name: String { get }
}
extension A {
    var name: String {
        "Deafult name"
    }
}
struct Foo: A {}
struct Bar: A {}

And now we wish for all types that conform to A (ie Foo and Bar) to also conform to CustomStringConvertible with default implementation. What I intuitively would try is

extension A: CustomStringConvertible {
    var description: String {
        name
    }
}
// Gives error: Extension of protocol 'A' cannot have an inheritance clause

However, that doesn't work. My question is

  1. why (what's the reasoning behind this), and
  2. how would I write this in correct, idiomatic swift?

Thanks for having a look!

There is no theoretical reason why it can't be done (although there may be constraints in terms of what can be implemented without breaking ABI); it simply hasn't been implemented. That said, to implement such a feature would be a very large undertaking and of great difficulty.

It's worth noting that a feature like this has been discussed as an extension to parameterized extensions (see the Future Directions section) where it would be spelled as:

extension<T: A> T: B {
    // Implementation for `B`
}

Note that parameterized extensions are themselves not yet a part of the language, so even if this direction were pursued it would be a long way off.

1 Like

Hm. I see...

The problem I see with this is (as far as my two cents are worth)

  1. it doesn't work as one would intuitively expect
  2. it makes a rather weak point in favor of protocol-oriented programming (vs using class inheritance), since subclassing would lend itself perfectly to the above example, as demonstrated in the snippet below

Snippet:

class A {
    var name: String {
        "Deafult name"
    }
}
class Foo: A {}
class Bar: A {}

extension A: CustomStringConvertible {
    var description: String {
        name
    }
}

Do I understand it correctly then, that if I were to chose to work with protocols (and structs) instead of classes I am well out of luck, and need to add CustomStringConvertible to each type by hand?

Depends on what you mean by "by hand". You can write the protocol extension with the implementation once, and then write out the conformance for each relevant type, e.g.,

protocol A {
    func foo()
}

protocol B {}

extension B {
    func foo() {
        // some reasonable default implementation
    }
}

extension Int: A, B {}
extension String: A, B {}
// etc.
1 Like

Protocol to protocol conformance was mentioned by @Douglas_Gregor in his Generic Manifesto.

There are some issues with it.

Today, only concrete types could conform to a protocol forming an acyclic graph which means that an constructed object in this graph has a finite fixed size containing all the super class methods or has a finite amount of associating methods corresponding to different conforming protocol methods.

This isn't anymore the case when protocols can extend protocols. The list of associated methods grow when shaping a protocol value by new conforming protocol type.

This is even the case when repeatedly coercing between A->B->A as B's associated method's default implementations can form a composition of A's associated method's and likewise A can do this with B's associated methods implying that a:A doesn't necessarily equate to a'=((a:A):B):A, so A->B->A can't be reduced to simply A in the chain.

The next problem that we may have conformances like this:

A->B->D
A->C->D

What is then the transitive result of a:D? You need some way to specify the path in the type annotation like a:D over B.

3 Likes

This is definitely a limitation of the language, but I found a way that can work a bit better than having to extend all the concrete types to conform to the protocol, you can create an adapter:

protocol A { var name: String { get } }
extension A { var name: String { "Default name" } }
struct Foo: A {}
struct Bar: A {}

struct ACustomStringConvertible: CustomStringConvertible {
  let a: A
  var description: String { a.name }
}

And then whenever you need to pass in a A as a CustomStringConvertible you wrap it with the adapter.

You can also create a static function in CustomStringConvertible to wrap an A to have a better autocomplete experience:

extension CustomStringConvertible where Self == ACustomStringConvertible {
  static func a(_ a: A) -> Self { .init(a: a) }
}

And then whenever there is a function that expects a CustomStringConvertible but you have an instance of A, you can use this static function to wrap it.

func print(_ customStringConvertible: CustomStringConvertible) {
  // do something with `customStringConvertible`
}

let myInstance: A = Foo()
print(.a(myInstance))

This may not be the best solution for CustomStringConvertible because there is some compiler magic with this protocol. But for other cases it can work well.

1 Like