Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SR-13465] Generics and hierarchical protocols don't work as expected #55907

Open
swift-ci opened this issue Aug 28, 2020 · 7 comments
Open

[SR-13465] Generics and hierarchical protocols don't work as expected #55907

swift-ci opened this issue Aug 28, 2020 · 7 comments
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. compiler The Swift compiler in itself

Comments

@swift-ci
Copy link
Collaborator

Previous ID SR-13465
Radar rdar://problem/67915515
Original Reporter sef (JIRA User)
Type Bug

Attachment: Download

Environment

macOS, Xcode 11.6 and later.

Additional Detail from JIRA
Votes 0
Component/s Compiler
Labels Bug
Assignee None
Priority Medium

md5: ac544037016382dfdb9161ffec028f5e

Issue Description:

In rewriting a medium-sized project, since my first attempt got too complicated when trying to deal with back-ends with incompatible requirements (er, as an example: Core Data and Firebase need to have different classes), I tried using protocols. That's their purpose, after all, to allow code to work on unrelated classes, as long as the classes conform to the right protocols. Most of my objects have a name and ID, and then some have more. So I created a hierarchical set of protocols, and then some generics to deal with them.

Only, it turns out, that the generics only dealt with the lowest protocol. That is, specifying something as someFunc<T: BaseProtocol>, when it might be given objects of BaseProtocol and objects of a descended protocol, will treat all of the objects of T as being only BaseProtocol.

I have attached an xcode project which is very stupid and dumb, but which demonstrates it (in the didSet property observer, every object given to it is of "AlbumProtocol," even the ones that are actually Tapes and use TapeProtocol).

This seems to significantly limit the uses, and also seems to run counter to principle of least astonishment. In particular, if I have overloaded functions, e.g.

func doSomething<T: AlbumProtocol>(object: T)

func doSomething<T: TapeProtocol>(object: T)

and call "doSomething(object)" inside the generic class... only the first one gets called.

@swift-ci
Copy link
Collaborator Author

Comment by Sean Eric Fagan (JIRA)

Hm, any hierarchies don't work, and this does seem like a pretty major problem.

{{}}
func doCheck(_ obj: Base) { print("Base") }
func doCheck(_ obj: Subclass) { print("Subclass") }
class Person<T: Base> {
var person: T
func doCheck() { doSomething(self.person) }
}
let x = Person<Base>("Wilma")
let y = Person<Subclass>("Barney")
x.doCheck() // prints Base
y.doCheck() // prints Base
doSomething(x.person) // prints Base
doSomething(y.person) // prints Subclass

@typesanitizer
Copy link

@swift-ci create

@eeckstein
Copy link
Member

Function overloads are always resolved at compile time.

Therefore if you call doSomething within Person.doCheck, the compiler selects the Base version, because at compile time it cannot know what T is.
Calling doSomething on x and y can be resolved at compile time, because x has type Person<Base> and y has type Person<Subclass>.

If you want dynamic dispatch at runtime, the function needs to be a protocol function (and not a global function).

@swift-ci
Copy link
Collaborator Author

Comment by Sean Eric Fagan (JIRA)

Two responses:

First, if you use derived classes instead of generics in my example, it works exactly as expected. That is, the subclass object go to a subclass-specific function, the base-class object goes to a base-class specific function.

Second, protocol functions –- unless I do not understand what you mean by the term – do not work either, as long as generics are involved. I in fact discovered this in a project that used protocols and generic classes and functions, and imagine just how much fun it was to discover that it was simply not possible to have hierarchical protocols, complete with extensions to implement default functions, and then find out that the classes I wrote that used them as generics would only use the one protocol or class I used in writing the generic.

So, to summarize: if you use classes and subclasses, then everything works exactly as one expects. If, however, you use generics, then it will always use exactly the class/protocol you specify in the generic, even when the object is a derived class, or if the object is constrained to derived protocols.

And you are not allowed to use the same name but with different protocol constraints when writing generic classes.

Please, if I've missed something obvious, show me. I spent a long time winnowing down the test case to the small attached project, feel free to show how it could be done.

@swift-ci
Copy link
Collaborator Author

Comment by Sean Eric Fagan (JIRA)

(Oh, and to point out: how I discovered this was in trying to use SwiftUI with generics – having one view for protocol Base, and another for protocol Derived. But that doesn't work. And it's not possible to check for protocol conformance at run-time, so instead I have to check for specific types at runtime, which means that as I add more back-ends for my data source, I have to check for each and everyone of them. Instead of simply saying ObjectDetalView(object: self.object), and letting the compiler and runtime decide whether that's an Album-protocol only, or a Tape-protocol, or a CD-protocol. Etc.)

@swift-ci
Copy link
Collaborator Author

Comment by Sean Eric Fagan (JIRA)

Ok, so by protocol function, you mean something like this?

import Foundation



protocol One {

    associatedtype idType

    var id: idType { get }

    var name: String { get set }

    func hello()

}



extension One {

    func hello() {

print("Hello, One")

    }

}



protocol Two: One {

    var age: Int { get set }

}

extension Two {

    func hello() {

print("Hello, Two")

    }

}



class Class1: One {

    var id = UUID()

    var name: String



    init(\_ name: String) {

self.name = name

    }

}



class Class2: Two {

    var id = UUID().uuidString

    var name: String

    var age: Int



    init(\_ name: String, age: Int = 10) {

self.name = name

self.age = age

    }

}



let x = Class1("Wilma")

let y = Class2("Pebbles", age: 2)



func show\<T: One\>(\_ obj: T) {

    obj.hello()

}



show❌

show👍

(Sorry, I don't seem to have figure out how to format text here very well.)

That does what I expect, if that's what you meant. Now, I'm not sure how it applies to the bigger problem I was running into, but at least it's less of a surprise.

@eeckstein
Copy link
Member

Yes, that's what I meant.
As a general rule: if you want dynamic dispatch of methods, you need to use class or protocol methods.
Overloaded functions are always resolved at compile time, but not at runtime.

@swift-ci swift-ci transferred this issue from apple/swift-issues Apr 25, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. compiler The Swift compiler in itself
Projects
None yet
Development

No branches or pull requests

3 participants