Method with generic constraint of generic classes not called

Apple Swift version 5.4.2 (swiftlang-1205.0.28.2 clang-1205.0.19.57)

Can someone explain to me what is happening here and why the constrained method is not called?

class MyTest<Output> {
    let myVar: Output

    init(myVar: Output) {
        self.myVar = myVar
    }

    func foo() {
        bar()
    }

    func bar() {
        print("'bar' without generic constraint")
    }

    func bar() where Output == Int {
        print("'bar' constrained to 'Int' output")
    }
}

let myTest1 = MyTest(myVar: 5)
myTest1.foo()

let myTest2 = MyTest(myVar: "String")
myTest2.foo()

The output is as follows:

'bar' without generic constraint
'bar' without generic constraint

Why is the 'bar' method with the generic constraint to the 'Int' type not called by 'myTest1.foo()' ?
Where does Swift lose the type info?

It gets really absurd when you call "bar" directly:

let myTest1 = MyTest(myVar: 5)
myTest1.bar()

let myTest2 = MyTest(myVar: "String")
myTest2.bar()

This leads to the following output:

'bar' constrained to 'Int' output
'bar' without generic constraint

I.e. in this case the type constrained method is being called.

I really don't understand what is going on here and why. It's like a method call inside the class type itself suddenly loses the type info and calls the non-constrained method.

1 Like

I found an ugly workaround, but can't imagine that's the best way to go:

    func foo() {
        if Output.self is Int.Type {
            (self as! MyTest<Int>).bar()
            return
        }

        bar()
    }

While this code works, the following one absurdly does not:

    func foo() {
        if Output.self is Int.Type {
            (self as! Self<Int>).bar()
            return
        }

        bar()
    }
1 Like

You're expecting dynamic dispatch, but in swift dynamic dispatch happens only in a few places:

  • protocol conformance
  • overrides in subclasses
  • objc stuff

In all other cases, the methods are dispatched statically, which means that in line 9 compiler has to choose a single function to call. Only the one from line 12 will work for all cases, so this one is chosen

Huh. I would've expected for it to be a compile-time error, because Self is not necessarily generic, and even if it was, it already has the generic argument filled in and you cannot tackle a second one on the end to make MyTest<String><Int>. Could be filed as a bug report to bugs.swift.org

Anyway, here's a workaround without force casting:

    func foo() {
        if let intSelf = self as? MyTest<Int> {
            intSelf.bar()
        } else {
            bar()
        }
    }
3 Likes

Okay, understood. But why does the call I mentioned use the constrained method then?

myTest1.bar()

This one does use the method constrained to the Int type. If it uses a dynamic dispatch then why does the call from inside the class not?

The type of myTest1 is MyTest<Int> which means that it's known at compile time that Output is Int. Because of that both versions of bar can work here, and compiler chooses the more specific one (the one at line 16)

It's still statically dispatched, but this time compiler has more information about the types

4 Likes

Previously filed as https://bugs.swift.org/browse/SR-14731 and fixed in Sema: Reject generic arguments applied to 'Self' by slavapestov · Pull Request #37891 · apple/swift · GitHub

3 Likes

Without knowing internals of the compiler this is in my opinion a very unintuitive behavior.

As a developer one could also have assumed that for generic classes several variants are created at compile time. If the variant with a special type was created at runtime, the type-constrained variant of a method could also be called without dynamic dispatch, since the variant and the methods to be called were already determined at build time.

What I find particularly dangerous is if you look at the following example:

class MyTest<Output> {
    let myVar: Output

    init(myVar: Output) {
        self.myVar = myVar
    }

    func foo() {
        bar()
    }

    private func bar() {
        print("'bar' without generic constraint")
    }

    private func bar() where Output == Int {
        print("'bar' constrained to 'Int' output")
    }
}

The type constrained private bar function is basically a dead method and will never be called, especially not for MyTest<Int> instances. You can't call it outside the class itself and since the method is not called by any other generic method the bar method without the type constraint will always be chosen due to the reasons you mentioned.

Such an implementation can be read in such a way that the developer had the intention to provide a special implementation of a method for a well-known generic type parameter.

So from a developer's point of view: how do you implement a generic class in Swift and tell the compiler to use a special implementation for well-known types of a generic parameter?

1 Like

I agree that this can be unintuitive and a bit cumbersome to work with. If what you want to achieve in your actual use case is to let the implementation of bar() depend on Output you can attach it to the output type:

class MyTest<Output: P> {
  let myVar: Output

  init(myVar: Output) {
    self.myVar = myVar
  }

  func foo() {
    Output.bar(for: self)
  }
}

protocol P {
  static func bar<Output: P>(for myTest: MyTest<Output>)
}

extension P {
  static func bar<Output: P>(for myTest: MyTest<Output>) {
    print("'bar' without generic constraint")
  }
}

extension String: P {}
extension Int: P {
  static func bar<Output: P>(for myTest: MyTest<Output>) {
    print("'bar' constrained to 'Int' output")
  }
}


func test() {
  let myTest1 = MyTest(myVar: 5)
  myTest1.foo() // 'bar' constrained to 'Int' output

  let myTest2 = MyTest(myVar: "String")
  myTest2.foo() // 'bar' without generic constraint
}
test()
3 Likes

@Jens This is an interesting way to do it. I didn't know this approach, so I think for a while and find a nit.

If one wants to do real work with MyTest.myVar in the function, he has to do forced type casting. Otherwise it can't compile, because while the Output generic type is atually Int, the knowledge is only in generic system, not in the protocol.

extension Int: P {
  static func bar<Output: P>(for myTest: MyTest<Output>) {
    if (myTest.myVar as! Int) == 10 { print("OK")}  // forced type casting
  }
}

If this is a common approach to use generic and protocol (I don't know if it is), it would be good if Swift can add support to get rid of the forced type casting (I don't know how).

2 Likes

Very interesting and thanks for sharing the solution!

I have successfully tested the approach and it also allows things like:

protocol P {
    static func bar<Output: P>(for myTest: MyTest<Output>, data: Data) -> Self?
}

extension P {
    static func bar<Output: P>(for myTest: MyTest<Output>, data: Data) -> Self? { nil }
}

extension P {
    static func bar<Output: P>(for myTest: MyTest<Output>, data: Data) -> Self? where Self: Decodable {
        try? JSONDecoder().decode(Self.self, from: data)
    }
}

extension Int: P {
    static func bar<Output: P>(for myTest: MyTest<Output>, data: Data) -> Self? {
        data.withUnsafeBytes { $0.load(as: Self.self) }
    }
}

extension P {
static func bar<Output: P>(for myTest: MyTest) {
print("'bar' without generic constraint")
}

This is the correct code.

Sorry, I made a mistake in my example above, there's no need to force type cast when correcting it like this:

class MyTest<Output: P> {
    let myVar: Output
    
    init(myVar: Output) {
        self.myVar = myVar
    }
    
    func foo() {
        Output.bar(for: self)
    }
}

protocol P {
    static func bar(for myTest: MyTest<Self>)
}

extension P {
    static func bar(for myTest: MyTest<Self>) {
        print("'bar' without generic constraint")
    }
}

extension String: P {}
extension Int: P {
    static func bar(for myTest: MyTest<Int>) {
        print("'bar' constrained to 'Int' output, type of myVar:", type(of: myTest.myVar))
    }
}


func test() {
    let myTest1 = MyTest(myVar: 5)
    myTest1.foo() // 'bar' constrained to 'Int' output, type of myVar: Int
    
    let myTest2 = MyTest(myVar: "String")
    myTest2.foo() // 'bar' without generic constraint
}
test()
2 Likes

Actually, if you use this variant you may cause other issues with classes conforming to P:

class MyOutput {
    let val: Int
    init(val: Int) { self.val = val }
}

extension MyOutput: P {}

This causes a compiler error:

Protocol 'P' requirement 'bar(for:)' cannot be satisfied by a non-final class ('MyOutput') because it uses 'Self' in a non-parameter, non-result type position

So I wouldn't say your first approach was a mistake :wink:

Thanks for the great solution! It turned out so simple.

BTW, I have the impression that Self doesn't work well with class inheritance (I mean its behavior is a bit confusing, though I forgot the details). But that's another topic.

Jens may have better suggestion. But does it work for you if you add final before your class definition?

Yes. I.e. the compiler error does give us a correct description.
As described, Jens' first solution does work with non-final classes though.

Thanks for the great solution

Terms of Service

Privacy Policy

Cookie Policy