Method with specialized generic constraints not filtered

Hey,

First post, I’m not 100% positive that this is normal Swift behavior or if it’s Swift not doing what I’m expecting.

Given the following:

protocol Baz: Equatable {
    
}

protocol Qux: Baz {
    
}

protocol BazTransformer {
    func transform<T>(baz: T) -> String where T: Baz
}

As a friendly reminder, Equatable has a Self requirement.

In an instance implementing BazTransformer, I’d like to specialize the generic constraint on T, as such to have a different logic on instances of Qux. As such:

struct SomeOtherTransformer: BazTransformer {
    func transform<T>(baz: T) -> String where T : Baz {
        return "baz"
    }
    
    func transform<T>(baz: T) -> String where T : Qux {
        return "qux"
    }
}

I’m expecting that if I call SomeOtherTransformer.transform(baz:) with an instance of Qux, that qux is returned, however only the Baz “version” is called.

I’m aware that the following would have worked, if there wasn’t the Self requirement:

protocol Foo {
    
}

protocol Bar: Foo {
    
}

protocol FooTransformer {
    func transform<T>(foo: T) -> String where T: Foo
}

struct SomeTransformer: FooTransformer {
    func transform<T>(foo: T) -> String where T : Foo {
        if let _ = foo as? Bar {
            return "bar"
        }
        
        return "foo"
    }
    
    func transform<T>(foo: T) -> String where T : Bar { //wouldn’t be called
        return "bar"
    }
}

Is this the expected behavior? If so, how could I mitigate it?

Thanks,

Clément

Swift only gives you "dynamic dispatch" behavior in three cases:

  • Calling a protocol requirement
  • Calling a (non-final) class member
  • Calling a function value / closure

where by "dynamic dispatch" I mean "the function body you're going to run is not decided based on the static type information at the call site".

So in this case, if you want the behavior to be different between "Baz-types" and "Qux-types", you have to make that a requirement on Baz itself. Your concrete types don't have to customize it, but it does have to be there.

protocol Baz: Equatable {
  // This is the new requirement.
  func transform<Transformer: BazTransformer>(using transformer: Transformer) -> String
}
extension Baz {
  func transform<Transformer: BazTransformer>(using transformer: Transformer) -> String {
    return transformer.transformImpl(self)
  }
}

protocol Qux: Baz {
}
extension Qux {
  func transform<Transformer: BazTransformer>(using transformer: Transformer) -> String {
    return transformer.transformImpl(self) // ***
  }
}

protocol BazTransformer {
  func transformImpl<T: Baz>(_ baz: T) -> String
  // You also need this one, so that the line marked "***" knows what to call.
  func transformImpl<T: Qux>(_ qux: T) -> String
}
extension BazTransformer {
  // This provides compatibility with the old interface.
  func transform<T: Baz>(baz: T) -> String {
    return baz.transform(using: self) // This calls the protocol requirement.
  }
}

struct SimpleBazTransformer: BazTransformer {
  // If a concrete type doesn't care about the difference,
  // this one function can satisfy both requirements.
  func transformImpl<T: Baz>(_ baz: T) -> String { return "" }
}

This is definitely a bit longer, though. We haven't come up with a good way to write this pattern more compactly (although note that it's particularly intricate because you have two separate protocols here, and you want to customize along both axes).


In the future, we'll probably implement the "generalized existentials" feature, which will allow your as? version to work. That's actually going to produce slightly less efficient code, since it has to go look up whether the argument conforms to Qux every time, but it is simpler and easier to maintain, which might be worth it.

3 Likes

Hey @jrose, thanks for your thorough reply!

I hope that the generalized existentials will be implemented in a future Swift version.

Is there a an on-going Swift Evolution proposal?

Thanks,

They're not really up to that point yet. People have been talking about them for a while, but since they're a fairly invasive and cross-cutting feature it'd probably need to be spearheaded by someone who's comfortable working across many levels of the compiler and runtime. In practice today that probably means someone from Apple, but we're all tied up with ABI stability and other tasks.

(If you're interested in the tricky parts, here's one: if you do get a Baz value out, can you use == with it? What's the rule that says you can or can't?)

Hey @jrose,

Alright, so it’s not going to be implemented in the foreseeable future I guess.

As for the tricky part, if I remember correctly, I can’t use == with it since it has a Self requirement