How to find out which function is being dispatched, especially for opaque types

Hello, new to the Swift community here and coming from Julia language background. On my little adventure with the Swift Programming Language official book I have been going through Protocols, Generics, Extensions, and finally Opaque Types. I stumbled upon the interesting example from the book where it defines a brand new protocol (if I understand correctly) called Container and then it extends the known Array type to conform to that protocol.

protocol Container {
    associatedtype Item
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}
extension Array: Container { }

The example then proceeds to demonstrate some interesting aspects of opaque types. Something like (after some edits by me)

extension Container {
  var isContainer: Bool { true }
}

func makeOpaqueContainer<T>(item: T) -> some Container {
    return [item]
}

let opaqueContainer = makeOpaqueContainer(item: 12)
print(type(of: opaqueContainer)) // Array<Int>
let twelve = opaqueContainer[0]
print(opaqueContainer.isContainer) // true
print(type(of: twelve)) // Int

At this point, again coming from Julia language background, a question came to mind: how can we tell at run-time which function exactly is being called. In Julia there exists a popular macro @which that can be placed on the same line with a function call to print out which method is being dispatched, and that can greatly help with debugging and with understanding how the compiler is reasoning about the code. Is there a Swift equivalent of such @which functionality? My quick search led me to this thread which is discussing some topics way over my head for a beginner, yet this particular comment helped me explore more:

extension Container {
  func whatAmI() { print("Container") }
}
extension Array {
  func whatAmI() { print("Array") }
}
func whatIsIt<C: Container>(_ value: C) {
  value.whatAmI()
}

let x = [1, 2, 3]
x.whatAmI() // Array
whatIsIt(x) // Container
let y: some Container = [1, 2, 3]
y.whatAmI() // Container
print(type(of: y)) // Array<Int>
print(y[1]) // 2

I'm particularly curious about the very last line. Is there a way to annotate that line with @which-similar construct to let us know what subscript function exactly is being called for that some Container, together with the exact concrete type that is being resolved by the compiler? For example

print(@which y[1]) // Array<Int>.subscript

Having such feature in the Julia language was really helpful and I'd very much like to have a similar feature to help me validate my reasoning about Swift code! Thanks in advance!

2 Likes

There is indeed no such functionality (not that I'm aware of), so you will have to look up or learn the rules.

Opaque types are actually are one of the easier cases, since they are simply syntactic(?) sugar. A function returning or a variable storing an opaque type (say, some Collection) does not return/store any collection, there's always a concrete underlying type known at compile time. For example, you can't write this:

var c: some Collection = [1, 2, 3]
c = ["Text"] // error: Cannot assign value of type '[String]' to type 'some Collection'

because c will be determined to be of type Array<Int> (but for the record, you won't even be able to assign any other integer array to c). The collection functions that you call on the opaque type will still be dispatched statically, because the underlying type can be resolved.

The reason why whatAmI(), when called on your y variable, resorts to the protocol implementation, is that you haven't defined it as a protocol requirement (i.e., it's only in your protocol extension). This is because you can extend indefinitely and do it in modules other than the source of the protocol itself, so there's no way for the compiler to know if Array does or does not implement a function that is not required by the protocol. If you require this function in the main protocol definition though, y.whatAmI() will print "Array".

1 Like

Opaque types are more complicated than just a syntactic sugar. The function dispatch for them is different than for the underlying type:

func test<T: Collection>(_ arg: T) {
    print("generic")
}
func test(_ arg: [Int]) {
    print("normal")
}

let opaque: some Collection = [1, 2, 3]
let specific: [Int] = [1, 2, 3]
test(opaque) // prints generic
test(specific) // prints normal
2 Likes

In an IDE like Xcode, you can use "jump to definition" at a call site, and it will take you to the declaration that was picked for that call site. This is a static property of code in Swift so you don't need to execute the program to know what declaration a call site refers to.

More and more often it seems that feature returns multiple results, often times for unrelated symbols. If there a logic to the ordering that can help inform which one will actually be called?

3 Likes

If it returns multiple results in Xcode, I believe that either indicates a failure of indexing, so Xcode is falling back to by-name lookup, or it indicates the result maps to a protocol requirement or class method with multiple dynamically-dispatched implementations.

Jump to definition can show multiple results in this case. Quick Help will always show the (single) declaration that was chosen by the type checker.

3 Likes

I understand it's being resolved statically at compile time (and quite similarly in Julia but using JIT compilation). Yet, would be great to have access to such information programmatically to validate the assumptions (even outside of Xcode, for example with Linux server-side Swift), and to help with the debugging/troubleshooting in complex projects. From further reading, it seems that Mirror is a similarly good tool to help reason about types, and would be helpful if something exists along these lines but for function dispatch.

3 Likes

I agree! I had a similar idea for a Google Summer of Code 2020 project, and I'm still very interested in helping someone work on such a feature.

3 Likes

Interesting, thanks. Guess that means doc comments are even more important so we can tell things apart, as there's no way to navigation to declaration from the quick help, just the file that contains it.

Terms of Service

Privacy Policy

Cookie Policy