Understanding Possible Implicit Conversion

In the code that follows, I noticed that the second print outputs Substring. When I plug the same expression as an implicit return in a function that returns a String an implicit conversion seems to take place (refer: someFunctionThatDoesNotFail).

However, when I assign the same expression to an intermediate constant and then return that constant, this implicit conversion does not happen. (refer the commented someFunctionThatFails).

Can anyone explain why?

let string = "some string"

print(type(of: string + string.dropLast()))  // String
print(type(of: string.dropLast() + string))  // Substring


func someFunctionThatDoesNotFail(_ string: String) -> String {
    // why no error on implicit return?
    // is this an implicit cast?
    string.dropLast() + string
}

print(type(of: someFunctionThatDoesNotFail(string)))  // String


// func someFunctionThatFails(_ string: String) -> String {
//     let res = string.dropLast() + string
//     // error: cannot convert return expression of type 'String.SubSequence' (aka 'Substring') to return type 'String'
//     return res
// }

PS: I hope it's not another one of the "top level" behavior issues that I seem to hit a couple of times.

1 Like

In order, here are the overloads your code uses:

  1. +(_:_:) | Apple Developer Documentation
  2. +(_:_:) | Apple Developer Documentation
  3. +(_:_:) | Apple Developer Documentation

It would really help if we were able to option- or command-click on operators.

3 Likes

To elaborate a bit further, Substring conforms to RangeReplaceableCollection, which among other methods contains:

static func + <Other>(lhs: Self, rhs: Other) -> Self where Other : RangeReplaceableCollection, Self.Element == Other.Element

When applied to arguments where Self == Substring and Other == String, this will produce Substring. Since String also conforms to RangeReplaceableCollection, it has the same operator available, and applying it when Self == String and Other == Substring will produce String.

Inside someFunctionThatDoesNotFail, the above overload is not viable, because it would produce Substring rather than String. There is another overload available, which is:

static func + <Other>(lhs: Other, rhs: Self) -> Self where Other : Sequence, Self.Element == Other.Element

We can apply this with Self == String and Other == Substring, producing String as desired, and so this overload is selected.

As for why the original uses of + are ambiguous, Swift has a bunch of rules for overload and type-checker solution ranking to pick the 'best' overload in many situations so that you will end up with something reasonable even if there's not one unique solution that would successfully typecheck.

3 Likes

Thanks @Quedlinbug, just curious, how did you track down which overloads are invoked? Stepping through the debugger?

Of course, also thanks to @Jumhyn for the detailed explanation. :slight_smile:

1 Like

I looked in my brain, using other parts of my brain. I have some ideas about how I would have gone about this if that had not been possible but they're not efficient or automated. Maybe @Jumhyn knows the right way?

1 Like

I maybe rephrasing you, but just so I understood, is this what happens?

In the commented someFunctionThatFails function, the assignment let res = string.dropLast() + string does not have information about the return type, hence Swift's "pick the best overload" algorithm can only infer it as a Substring, as implicit conversions aren't a thing in Swift, it fails?

Even better, ask Xcode to explain how the resolution will be performed.