1

I'm trying to write a library that uses templated/generic type dispatch, but I can't figure out how overload resolution works in Swift. (Is there a more technical reference than The Swift Programming Language?)

The following works exactly as I'd hope:

func f(_ v: String) { print(v) }
func f(_ v: String?) { f(v!) }
func f(_ v: Int) { print(v) }
func f(_ v: Int?) { f(v!) }
f("foo")
f(Optional("bar"))
f(2)
f(Optional(3))

And this code also works the same way:

func g<T>(_ v: T) { print(v) }
func g<T>(_ v: T?) { g(v!) }
g("foo")
g(Optional("bar"))
g(2)
g(Optional(3))

But when I compile this:

func h(_ v: String) { print(v) }
func h(_ v: Int) { print(v) }
func h<T>(_ v: T?) { h(v!) }
h("foo")
h(Optional("bar"))
h(2)
h(Optional(3))

I get the warning

all paths through this function will call itself
func h<T>(_ v: T?) { h(v!) }

And sure enough executing it with an optional parameter blows the stack.

My first zany theory was that maybe generics don't participate in overload resolution with non-generics, but this:

func i<T: StringProtocol>(_ v: T) { print(v) }
func i<T>(_ v: T?) { i(v!) }
i("foo")
i(Optional("bar"))

gives me the same warning, and the second invocation blows the stack.

If v is of type Optional(String), I don't even understand how v! can be passed to a generic expecting a T?.

rvcx
  • 51
  • 4
  • I guess it's worth noting that replacing the unconditional `h(v!)` with a slightly more realistic `if let x = v { h(x) }` eliminates the compiler warning but still blows the stack when executed. – rvcx Dec 17 '20 at 22:15
  • 2
    `T` is unconstrained... so, it could accept an optional as well. Then the nested parameter would become a double optional – New Dev Dec 17 '20 at 22:23
  • 1
    Be very careful with "templated/generic type dispatch." You can easily break what generics promise. If you create any ambiguity, do not assume that Swift will call the most specific thing that matches at runtime. It will depend on what Swift can prove is true at compile time. If your types overlap in any way (array vs collection for example), you need to ensure that the behavior will be identical, with the only difference being performance. You should start with the specific code you are trying to remove duplication from. There isn't a single ("generic") way to do this. – Rob Napier Dec 18 '20 at 00:10

2 Answers2

2

This is happening because in your generic implementation there won't be any type inference happening. You could solve this by doing some optional casting (to avoid crashes) and sending it to the correct method, which will actually happen automatically anyway. So something like:

func h<T>(_ v: T?) { 
  if let stringValue = v as? String {
    h(stringValue) 
  } else if let intValue = v as? Int {
    h(intValue) 
  }
}

Maybe kinda defeats the purpose, but I can tell you as a Swift developer with several years of experience this type of generic handling doesn't/shouldn't really come up all that often in a real application if the rest of your code is written in a Swift-friendly way. I suppose that's a bit opinion-based, but there it is.

As for this comment:

If v is of type Optional(String), I don't even understand how v! can be passed to a generic expecting a T?.

Based on your implementation you are declaring that v: T?, which means that v must be either of type T or it is nil. Therefore v! is just you (as a developer) guaranteeing that v will be of type T, and not nil, and if you're wrong the program will crash.

I assume you are doing this just for learning purposes, but that is the biggest thing I notice in your example code - there would never be any point of having a method accept an Optional argument if you are going to immediately use ! to force unwrap.

creeperspeak
  • 5,403
  • 1
  • 17
  • 38
  • See the comment above; I boiled the sample down to merely demonstrate the problem. Using `if let x = v { h(x) }` is more like what the real code would do. But the real point is that I don't want to mix the code specific to handling optionals in with a list of all the other types the library handles. (In fact, handling `Optional(Optional(Optional(String)))` and such is perfectly reasonable, so enumerating them all is infeasible.) – rvcx Dec 17 '20 at 22:28
  • 1
    if you are using if let and not explicitly casting to `String`, the type of unwrapped value will still be `T` – Witek Bobrowski Dec 17 '20 at 22:29
1

I'd love a more direct answer to how generics work, but Swift's protocols and extensions provide a different approach to the same kind of dispatch.

protocol H { func h() }
extension String: H { func h() { print(self) } }
extension Int: H { func h() { print(self) } }
extension Optional: H where Wrapped: H { func h() { self!.h() } }
"foo".h()
Optional("bar").h()
2.h()
Optional(3).h()
Optional(Optional(3)).h()

Unfortunately, this dispatch mechanism cannot, as far as I know, be applied to tuples (or function types), as they cannot be extended.

rvcx
  • 51
  • 4
  • 1
    This is definitely on the right road, using explicit protocols that call out what you mean (though of course doing this with Optional would be horrible Swift, but it does demonstrate the general point of protocol extensions). As you note, there are many things tuples cannot do, which is why they are not a preferred type except for very small, short-lived uses. Name them (i.e. make a struct), and the issues go away. – Rob Napier Dec 18 '20 at 00:17