Protocols on bound generic types

I think this is similar to the issues addressed in SE-0361, but perhaps is still an issue?

I've got a generic that I've bound in two different ways (with Int vs Float as the bound type). I can't seem to add conformance to CustomStringConvertible to each concrete type. Using Swift 5.7, I'm getting the error "Conflicting conformance of 'Wrapper' to protocol 'CustomStringConvertible'; there cannot be more than one conformance, even with different conditional bounds".

Is there a way to do this?

Here's some example code to show this.

public class Wrapper<CoreType> {
    public var core: CoreType
    
    init(core: CoreType) {
        self.core = core
    }
}

extension Wrapper<Int> : CustomStringConvertible {
    public var description: String {
        return "Wrapped version of Int with value \(self.core)"
    }
}

extension Wrapper<Float> : CustomStringConvertible {
    public var description: String {
        return "Wrapped version of Float with value \(self.core)"
    }
}

1 Like

A type can conform to a protocol in only one way—this is by design. You are indeed using the shorthand now allowed by SE-0361 to spell a conditional conformance, but it’s just a shorthand; I’m not sure what you understand to be “the issues addressed” in that proposal as it is purely about syntax.

In this case, you can use a single conditional conformance where the associated type is constrained to conform to some protocol shared in common by all types you want to support, in this case Int and Float (though chances are that you’d be able to support all signed numeric types).

1 Like

Thanks for the quick reply!

So I understand what you're describing a bit better, what's the longhand version of the conditional conformance that I'm using? Do you mean the following?

extension Wrapper : CustomStringConvertible where CoreType == Int

instead of

extension Wrapper<Int> : CustomStringConvertible

If that syntax is mainly what SE-0361 is talking about, then first of all, I appreciate that syntactic help. So thanks to the people who made it happen! But secondly, I misunderstood SE-0361. Sorry! Maybe I could have said it's just talking about issues close to this, maybe?

Anyway, for the purposes of the statement that a "type can conform to a protocol in only one way", it sounds like you are saying that Wrapper<Int> and Wrapper<Float> are considered the same type. Am I understanding correctly?

Yes, you're correct!

Correct, for the purposes of protocol conformance, Wrapper can conform to any particular protocol in exactly one way or not at all.

1 Like

Thank you. On re-reading the proposal SE-0143 you linked to, I see the part that explicitly says "the ban extends even to multiple conformances that are 'clearly' disjoint" and uses a wrapper example similar to mine.

After thrashing at this for a while, I don't see a way around this ban. But at least the ban is easy to understand, once you've accepted all the ramifications. :grin:

It makes me so sad that we can't combine the use of generics and protocols in the interesting ways that we "ought to be able to". This is one of the most interesting aspects of Swift! Hopefully, someday the "for consistency" reasoning in the proposal will be reconsidered.

And it seems like a potential source of surprise conflicts (eg you update some external library and it now happens to conforms a generic that you use to a protocol that you use, and suddenly your own code is illegal, even though the final types are disjoint). I'm new to swift, so that scenario may be invalid. But hopefully you understand what sort of thing I'm talking about.

Anyway, thanks for clearing this up!

The solution, as Xiaodi mentioned briefly above, is to find a protocol that captures the constraints on CoreType for Wrapper to be able to provide an implementation for CustomStringConvertible (or define one yourself) and then provide a single conditional conformance for Wrapper, e.g.:

extension Wrapper: CustomStringConvertible where CoreType: CustomStringConvertible {
  public var description: String {
    return "Wrapped version of \(CoreType.self) with value \(self.core)"
  }
}

It is not considered the best practice to write 'retroactive conformances' (i.e., conform a type you don't control to a protocol you don't control) for exactly the reason you note. This has historically been a place where Swift probably does not adequately prevent the user from making problematic decisions, but SE-0364 (returned for revision recently) is an attempt to move us in a direction where users at least won't make this decision accidentally.

3 Likes

Thank you for expanding on Xiaodi's answer! Your example made the solution he was suggesting crystal clear. I think I adequately understand the motivation for that answer as well.

I hope I'm conveying that as a coder, it is quite unexpected to not be able to take control of a type that I am defining (or at least doing my best at defining, given that I'm just binding a generic to a type), and get it to conform to a protocol in a way that I want/need, without all these unintended consequences. I would happily do whatever is needed to avoid those consequences, but the language isn't giving me the tools to do so. It feels strange that if I want a List bound to a particular type, I can't get that list to conform to a protocol without that having repercussions on every other user of the List generic.

I do see the danger and why the policy has been defined the way it has. In fact, the whole extension mechanism, while incredibly powerful and useful, has been making me nervous. I see sample code on the net that adds extensions to List, and well, it gives me butterflies. I'm reminded of some of the creative stuff I see in the javascript world (though it's clearly not the same). I'm sure these issues have been thoroughly considered by many people, so I trust it's going to be mostly OK. :grin:

But getting back to the matter at hand, despite seeing the sense in current policies, I also see the need for doing the sort of thing I've described and I see why it feels like such an "ought to be able to" feature.

Yes, retroactive conformances are probably not a great idea. I hope it's clear, though, that that's not the intention of the coder in the situations I'm describing, but rather they are the consequence of the coders expressing their intentions in the way the language features are directing them.

I don't at all know what's going on under the hood with generics, but I guess what I'd be looking for, as a coder, would be a way to silo off a particular Wrapper<Int> that would be distinct from someone else's Wrapper<Int> and could therefore have its own table of extensions and protocol conformance, etc. typealias is the closest to this I know in Swift right now, but it don't think it differentiates mine from someone else's in the desired way. I'm guessing it's just syntactic sugar.

However, I guess I'm not done with my thrashing, as I've thought of two potential solutions that allow me to get the results I'm seeking, at least in the example situation. Both rely on taking control of a copy of the generic by making an empty subclass, thus getting, I'm guessing, a distinct virtual table or whatever swift uses to keep track of the protocols for the type.

Aside: The original Wrapper<Int> vs Wrapper<Float> example is quite contrived, of course, and just used to illustrate how the bound generics are affecting other bindings. So for Solution 1 below, I've simplified it to only show a single binding. For Solution 2, I've used Xiaodi's suggestion.

Solution 1: Make a new class based on the bound generic, then add protocol conformance to that new class.

public class FloatWrapper : Wrapper<Float> {}

extension FloatWrapper : CustomStringConvertible {
    public var description: String {
        return "Wrapped version of Float with value \(self.core)"
    }
}

Solution 2: Subclass the unbound generic, then add protocol conformance to that new generic.

public class SiloedWrapper <CoreType> : Wrapper <CoreType> {}

extension SiloedWrapper : CustomStringConvertible {
    public var description: String {
        return "Wrapped version of \(CoreType.self) with value \(self.core)"
    }
}

These solutions avoid retroactive conformance on the original Wrapper generic, but they only work for non-final classes that don't already conform to the protocols in question. It would be great if there was something like typealias that did something like the above and fixed whatever problems might arise in my proposals (including working on types other than class) and relaxed some protections as allowed by the new context. For example, maybe the new type could override some extensions/protocols from the superclass, since it's clearly mine now.

Finally, I'll share a worry that's giving me some doubts about my proposals: what happens when you need to use an object of these siloed types in a context that expects one of the original type. How does type-casting work? Maybe these solutions won't work for many situations.