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!
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
why (what's the reasoning behind this), and
how would I write this in correct, idiomatic swift?
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.
The problem I see with this is (as far as my two cents are worth)
it doesn't work as one would intuitively expect
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.
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.
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.