Let's say I want to define a strongly typed UIView wrapper that adds padding to another view:
public final class PaddedView<Content: UIView>: UIView {
public init(
content: Content,
padding: UIEdgeInsets
) {
self.content = content
self.padding = padding
addSubview(content)
// Set up constraints
}
public let content: Content
public let padding: UIEdgeInsets
}
Then I want to make a convenient extension to pad a view:
public extension UIView {
func padded(with padding: UIEdgeInsets) -> PaddedView<Self> {
.init(content: self, padding: padding)
}
}
This does not compile, and produces the following error:
Covariant 'Self' or 'Self?' can only appear at the top level of method result type
I recently learned of a trick to work around this (by investigating how the publisher<T>(for: KeyPath<Self, T>) extension on NSObject is implemented). We introduce a protocol representation of the class:
public protocol UIViewProtocol: UIView {}
Then extend the class to conform to the protocol:
extension UIView: UIViewProtocol {}
Then change the generic parameter and the extension to use the protocol:
public final class PaddedView<Content: UIViewProtocol>: UIView {
...
}
public extension UIViewProtocol {
func padded(with padding: UIEdgeInsets) -> PaddedView<Self> {
.init(content: self, padding: padding)
}
}
Covariant Self is allowed to appear anywhere in a function signature on a protocol, so this now compiles, and it achieves the intended result (if I call let result = something.padded(padding: ...), I can query result.content and get back the correctly typed something).
That made me curious: why does this restriction exist on classes but not on protocols? It seems, although I haven't thought deeply about it, than any time you'd get stuck on this limitation with classes, you can introduce this protocol equivalent trick and surmount it, which makes it seem like the compiler has already solved the problem it would need to solve to allow the same with the class directly.
Maybe it has to do with existentials? What happens if I had an any UIViewProtocol and I tried to call .padded on it? Well I can't. Generic parameters can't be covariant, so this appearance of Self is in a non-covariant position, which means this extension gets deleted from the existential. Existentials of protocols that use covariant Self didn't exist at all until Swift 5.7, and extensions that introduced such a use were deleted from existentials. Perhaps forbidding anything but the only possible covariant ("top level" of return value) use of Self in a class extension was an older approach to avoiding this problem, on par with forbidding the comparable protocol existentials entirely?
But what's interesting is you can still "erase" a subclass up to its base class: let erasedView: UIView = UILabel(), and the extension is available, and you'll get an instance of PaddedView<UIView> back (that's not just the static type of the return value but its actual dynamic type too). So apparently the existential that gets opened when going into this extension is the static type if that's a class, but the dynamic type if it's a protocol. The extension isn't available on a protocol existential here, but would be if I changed the return type to UIView. Then if I declared erasedView as an any UIViewProtocol, the result of erasedView.padded would have a static type of UIView but a dynamic type of PaddedView<UILabel>. This does not happen if I make the return type UIView and extend UIView instead of UIViewProtocol (the static type would be UIView and the dynamic type would be Padded<UIView>). So introducing the protocol enables me to at least end up with the "correct" dynamic type (bound to the actual dynamic type of the receiver), which isn't possible if I extend the class.
Why couldn't it (or shouldn't it) work that way with the class directly, where it would simply bind the generic parameter of the return type to the known static type of whatever the extension is called on? That is, why can't I put this extension on UIView, and then when I call something.padded, it just binds Self to the static type of something?
Does it have something to do with the way dispatch is implemented? I've been caught off guard before by how dispatch works in conditional extensions on generic types, and concluded that generic concrete types must have a singular representation under the hood: one method table (for both statically and dynamically dispatched member functions) for the type itself, which is very different than, say, C++, where each specialization of a template class is its own full-blown class with its own compiled methods (that makes constrained method overloading possible). I may be remembering this incorrectly, but I recall being unable to make conditional overloading work on a generic struct, but when I replaced the generic struct with a protocol with an associatedtype and hung the extensions off of that, I was able to make it work.