Reputation: 17231
I use Swift's nil-coalescing operator in a chain to print one of three values. The first two are optional, the last one is non-optional:
let optionalString: String? = nil
let optionalError: Error? = MyError.anError
let defaultString: String = "default"
print(optionalString ?? (optionalError ?? defaultString))
I was surprised to see that this code prints
Optional(MyError.anError)
to the console. I was expecting a non-optional:
MyError.anError
The error is defined as follows:
enum MyError: Error {
case anError
}
I know that there are two implementations of the nil-coalescing operator:
func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T) rethrows -> T
and
func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T?) rethrows -> T?
According to this, ??
should return an optional (T?
) only if the defaultValue
is an optional as well (T?
). But this is not the case:
The default value for the first ??
operator is (optionalError ?? defaultString)
. To evaluate this value's type, we need to evaluate the result of the second ??
operator. As defaultString
is of type String
and thus a non-optional, function (1) is used for the nil-coalescing operator. Hence, the result of (optionalError ?? defaultString)
must be non-optional as well, which is the default value for the first ??
operator. So again, function (1) is used for the nil-coalescing operator and thus, optionalString ?? (optionalError ?? defaultString)
should return a *non-*optional.
Apparently, there is a flaw in my logic, but I don't see it. Any explanation for this?
Upvotes: 1
Views: 672
Reputation: 17231
Disclaimer: I wasn't sure if I should add this as a supplement to my question or if I should post it as an answer. I decided for the latter. It's not a bullet-proof explanation, but I think it's "close enough" for many who might run into the same problem.
The two comments stating that the code provided doesn't compile for the commentators pushed me in the right direction:
I copied the code in a blank playground and ran it. For me, it did compile, but it also showed a warning which it didn't show before (because for some reason, Auto completion and compiler warnings were broken in the original project).
The warning
Expression implicitly coerced from 'Any?' to 'Any'
tells me that the expression (optionalError ?? defaultString)
is apparently evaluated as type Any?
, which is an optional. The compiler then chooses the ??
operator function (1) to evaluate the entire expression which requires that its default value (optionalError ?? defaultString)
is non-optional. Thus, this value is implicitly coerced from Any?
to Any
. And that's why Optional(MyError.anError)
is printed.
There are two questions remaining:
(optionalError ?? defaultString)
evaluated to Any?
?I don't have an answer for question 1,
but my guess would be that the compiler simply assigns a higher precedence to function (1) when the parameter types don't match and it's unclear which function to use. (I'll be happy to be corrected.)
Regarding question 2:
optionalError
is of type MyError?
and defaultString
is of type String
. The signature of the ??
operator functions read:
func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T) rethrows -> T
func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T?) rethrows -> T?
So both parameters, optional
and defaultValue
must have the "same" generic type (only that in the first case, one parameter is an optional of that particular type and the other one is not). MyError
and String
are obviously not the same type. So in order for this to match the criteria of the function signature, the compiler must coerce both types to Any
.
So MyError?
is coerced to Any?
and apparently, String
is also coerced to Any?
and thus, function (2) is used which evaluates (optionalError ?? defaultString)to
Any?`.
That again raises the question why function (2) is used in this case which kind of contradicts my assumption for question (1), but my main take-away here is that it's simply a bad idea to use two different types with the nil-coalescing operator.
I got this code example from a textbook on RxSwift which is slightly different as it uses a generic type that is required to conform to CustomStringConvertible
:
func print<T: CustomStringConvertible>(label: String, event: Event<T>) {
print(label, event.element ?? event.error ?? event)
}
What got me experimenting with this in the first place was that when I ran the code from the book it printed an optional whereas the book claimed that it would print a non-optional.
Upvotes: 1