Is this an inappropriate use of @_disfavoredOverload?

Howdy,

I have an API that looks like this

protocol Base {}

enum BaseOption {
  case base
}

extension Base {
  func foo(_ option: BaseOption) {
     print("Base")
  }
}

protocol Refined: Base {}

// Basically an extension to BaseOption
enum RefinedOption {
  case base, refined
}

extension Refined {
  func foo(_ option: RefinedOption) {
    switch option {
      case .base:
        // Call into base impl
        (self as Base).foo(.base)
      case .refined:
        print("Refined")
    }
  }
}

struct Thing: Refined {}



let x = Thing()

x.foo(.base)  // > Base
x.foo(.refined)  // > Refined
(x as Base).foo(.base)  // > Base
(x as Base).foo(.refined)  // > error: type 'BaseOption' has no member 'refined'

So far everything is working as I desire. A Refined is just Base with some extra options for foo. Where this falls apart however is when I introduce a second refinement on Base...

protocol Refined2: Base {}

enum Refined2Option {
  case base, refined2
}

extension Refined2 {
  func foo(_ option: Refined2Option) {
    switch option {
      case .base:
        // Call into base impl
        (self as Base).foo(.base)
      case .refined2:
        print("Refined2")
    }
  }
}

extension Thing: Refined2 {}

x.foo(.refined)  // > Refined
x.foo(.refined2)  // > Refined2
x.foo(.base) // > error: ambiguous use of 'foo'

Now, I'm aware that the underscored attribute @_disfavoredOverload can "solve" this issue, but I am unsure if this is really a good idea. And the following quote from the linked documentation doesn't exactly inspire confidence...

Use @_disfavoredOverload to work around known bugs in the overload resolution rules that cannot be immediately fixed without a source break. Don't use it to adjust overload resolution rules that are otherwise sensible but happen to produce undesirable results for your particular API; it will likely be removed or made into a no-op eventually, and then you will be stuck with an overload set that cannot be made to function in the way you intend.

Since the ambiguity on foo seems reasonable to me, this leads me to believe that I should not be using @_disfavoredOverload here, but I would like that confirmed.

Also, as a follow-up question if I should not be using @_disfavoredOverload, how could I design an API that has a similar shape?

Thanks,
smkuehnhold

what is different about BaseOption.base and Refined2Option.base?

if they are the same, you probably want to refactor your enums so that “base” has one canonical representation, because it sounds like you want BaseOption.base == Refined2Option.base, and that’s bogus because BaseOption and Refined2Option are unrelated types.

1 Like

if they are the same, you probably want to refactor your enums so that “base” has one canonical representation, because it sounds like you want BaseOption.base == Refined2Option.base, and that’s bogus because BaseOption and Refined2Option are unrelated types.

Can you clarify what you mean by "one cannonical representation"? What would that look like?

It's not necessarily the case that BaseOption.base must equal Refined2Option.base, and I am aware that is not how it is now (however it would be nice if that were true). What I am really looking for is a way to extend a similar looking API with additional options in my refined types. Any "BaseOption" found in Refined2Option will be manually forwarded to Base's implementation. In other words, if Swift had syntax that allowed enums to inherit from one-another, that is what I'd be reaching for.

I think it means don't have a .base case in RefinedOption or Refined2Option. Then there's only one possible function that can be chosen for x.foo(.base)

1 Like

Oh. That makes too much sense :joy:.

Thanks @taylorswift & @jeremyp !