Extension on Generic Type allows overriding behavior

Came across an issue today that seems to violate an expectation I had on how extensions work.

According to the language guide:

Extensions add new functionality to an existing class, structure, enumeration, or protocol type.
Extensions can add new functionality to a type, but they can’t override existing functionality.

Given

struct Foo {
    func bar() { print("bar") }
}

As expected, the following extension will produce a compiler error: Invalid redeclaration of 'bar()'

extension Foo {
    func bar() { print("bazz")  } // compiler error
}

However, if we make Foo Generic:

struct Foo<T> {
    func bar() { print("bar") }
}

As in the previous example, this extension will also generate a compiler error

extension Foo {
    func bar() { print("bazz") } // compiler error
}

However, if we specialize the extension, we don't get a compiler error and we can override the functionality of bar()

extension Foo where T == String {
    func bar() { print("bazz") }  // NO compiler error
}

You can only define this overriding method once, if you try to create another extension on Foo where T == String, you will get the same Invalid redeclaration of 'bar()'error.

Initially this doesn't seem totally crazy. Foo<String> and Foo<Int> aren't technically the same type, but it seems like a violation of extension design to not override existing behavior. For instance you can do this:

extension Array where Element == Int {
    func append(_ newElement: Element) {
        print("append is not allowed")
    }

    func insert(_ newElement: Element, at:Int) {
        print("insert is not allowed")
    }
}

This completely changes the implementation of the Array Type, which extensions shouldn't be able to do.

Is this a bug in the compiler or is this the expected behavior? The Language guide doesn't call this out anywhere that I could find. It seems to me like it's a bug.

It seems like maybe the Generic Type function declarations get treated like protocol extensions?

protocol Foo {
    associatedtype T
    var value: T { get }
    func bar()
}

extension Foo {
    func bar() { print("Bar") }
}

struct SomeFoo<T>: Foo {
    let value: T
  
    func bar() { print("SomeBar") }
}

let fooInt = SomeFoo(value: 1)
fooInt.bar() // Bar
let fooString = SomeFoo(value: "bar")
fooString.bar() // SomeBar

Trying to understand if this override behavior on Generic Types is intended or this is a bit of a loophole that needs to get fixed in a future version.

1 Like

This behavior is intended, but it is not an override. These extension members can shadow others of the same name when you utter it and the extension member is deemed to be “more specific,” but they do not participate in dynamic dispatch unlike your last example and do not witness protocol requirements which already have implementations.

5 Likes

Is there an explanation for why this is the intended behavior? It seems like shadowing is specifically not allowed on the non-generic type. In this case, the "more specific" implementation is not additive, it changes (aka overrides) the default implementation.

To clarify, such an extension method is additive and it does not override the “less specific” implementation, which is only shadowed:

struct Foo<T> {
    func bar() { print("bar") }
}

extension Foo where T == String {
    func bar() { print("bazz") }
}

func bar<T>(_ t: Foo<T>) {
    t.bar()
}

let x = Foo<String>()
x.bar() // prints “bazz”
bar(x)  // prints “bar”

Being shadowed here means that, when you’ve got a value with static type Foo<String>, there is no way for you to spell a call to the implementation of Foo.bar that prints “bar”, but the implementation is still very much available. You can call a function that always in turn calls that implementation of Foo.bar, as shown here, and it will have no trouble invoking that method on a value of type Foo<String>.

Contrast this with a version of the last example in the original post:

protocol Bar {
    func bar()
}

extension Bar {
    func bar() { print("Bar") }
}

struct Foo<T>: Bar {
    func bar() { print("Bazz") }
}

func barbar<T: Bar>(_ t: T) {
    t.bar()
}

let x = Foo<String>()
x.bar()    // prints “Bazz”
barbar(x)  // prints “Bazz”

Notice how the default implementation printing “Bar” here truly has been overridden. This is because Bar.bar, as a protocol requirement, is dynamically dispatched.

This is the same behavior as you get when you override an instance method in a class:

class C {
  func f() { print("c") }
}

class D: C {
  override func f() { print("d") }
}

func f(_ c: C) {
  c.f()
}

let d = D()
d.f() // “d”
f(d)  // “d”

Again, notice how the base implementation is truly overridden. This is because overridable instance methods are also dynamically dispatched.

Now, let me blow your mind with a fact about protocol extension methods. Take the second code block in this post and remove bar as a protocol requirement:

protocol Bar {
    // Comment out this line: func bar()
}

extension Bar {
    func bar() { print("Bar") }
}

struct Foo<T>: Bar {
    func bar() { print("Bazz") }
}

func barbar<T: Bar>(_ t: T) {
    t.bar()
}

let x = Foo<String>()
x.bar()    // prints “Bazz”
barbar(x)  // prints ?

Protocol extension methods (as opposed to default implementations of protocol requirements) are not dynamically dispatched; another way of saying it is that they aren’t customization points like protocol requirements are; and another way of saying that is that these extension methods can be shadowed but not overridden. See if that helps you to reason about what “?” should be above and why.

7 Likes

Thanks @xwu, this was more clear. Specifically the example of still being able to call the original implementation. My original thinking was the default implementation was no longer available with the specialized extension.

func bar<T>(_ t: Foo<T>) {
    t.bar()
}

This still feels a little strange that you can change the behavior of append or insert(_, at:) through an extension, but it's more clear why it's doable and doesn't seem to violate the language guide than I originally thought.

Thanks again. :clap:

Hopefully this isn't too much of a derailment, but during a call to bar<String>(:) is t not statically known to be of type Foo<String> or am I mistaken? Does static here mean something else than "known at compile time"?

I'll appreciate any clarification :sweat_smile:. I have a lot left to learn.

No, because the semantics for Swift generics are such that the body of bar(_:) is type-checked (including the resolution of overloads) independently of any concretely-typed call to bar.

2 Likes

Does this mean that bar<T>(_:) is compiled down into a single generic function, or are we still getting a new function per type? :thinking:

Really, the answer is "both." The formal semantics of Swift generics are as-if there is no C++-style monomorphization going on, but the compiler is free to monomorphize ("specialize") as an optimization when it is able to (so long as it maintains the original semantics).

4 Likes