Unexpected behavior when resolving `map` on `Optional<Sequence>`

Below is a minimal example:

let x: String? = "abc"
let xPrefixString = x?.prefix(2).map(String.init)
print(xPrefixString as Any)  // Optional(["a", "b"]); should be (?) Optional("ab")

let y: String? = "def"
let yPrefixString = (y?.prefix(2)).map(String.init)
print(yPrefixString as Any)  // Optional("de")

Because things are (supposed to be) evaluated left to right, I would expect the additional parentheses used in the second case to have no effect and the two print statements above to print the same thing. However in the first case, while x?.prefix(2) has type Optional<Substring>, the map applied is Sequence.map, not Optional.map.

I would expect that to unwrap the Optional<Substring> and call Sequence.map on it, I would have to do x?.prefix(2)?.map(String.init); however this is a compiler error (Cannot use optional chaining on non-optional value of type 'String.SubSequence' (aka 'Substring')). Extra parentheses are required for that: (x?.prefix(2))?.map(String.init).

1 Like

This is interesting. It does make sense based on the rules of optional chaining, but it doesn't seem right at first. Even having sorted out why it is happening, it's a bit odd.

What are those rules of optional chaining? And isn't it a problem that left associativity is violated here?

?. isn't a simple pairwise operator; its value depends on everything to the right of ?. within the current expression.

For example, the value of x?.foo().bar() is either nil or Optional(x!.foo().bar()). There is no intermediate result that has the value Optional(x!.foo()). The intermediate values remain unwrapped through the entire pipeline.

However in the first case, while x?.prefix(2) has type Optional<Substring> , the map applied is Sequence.map , not Optional.map ... I would expect that to unwrap the Optional<Substring> and call Sequence.map on it, I would have to do x?.prefix(2)?.map(String.init)

You have the right idea, but your description applies to case two, not case one. When you parenthesize the prefix (y?.prefix(2)), then and only then is an Optional<Substring> involved in the evaluation. And you can indeed make case two have the same result as case one by adding another ?:

let yPrefixString = (y?.prefix(2))?.map(String.init)

Does that clarify?