Great points. One that you didn't mention but that I think is also important is How does the proposed solution compare to alternatives?.
I think you've done a reasonable job motivating that there is a problem to solve here, but there is a pretty big design space for library-only solutions.
What do you think of the recently suggested sequence.orEmpty sort of computed property? It would provide a sequence projection, so it has none of the problems that you outline above, and is much better (imo) from a readability/discoverability perspective.
If we had it, we could even have the compiler suggest that in a fixit for the common error where it isn't used, leading programmers to the solution with the result that the code is more immediately obvious to someone who didn't write it.
Personally, such a property would seem to encode the equivalence of optional sequences to empty sequences into the language. I think there's a distinction we should keep, and is with the degenerate optional chaining syntax.
I think orEmpty is easier to read and understand. But “or” anything just sounds like a non-generic form of “??”. It seems to be a suggestion mainly because of compiler shortcomings (is that right?) which feels a little odd. But maybe it’s the most pragmatic approach.
It requires awkward parentheses when the expression involves an optional chain, as?, try?, etc. As has been discussed, stored optional collections are almost always a bad idea so these expressions are some of the primary use cases a convenience feature would target.
IMO it would be unfortunate to introduce a convenience feature which is awkward to use in some of the most relevant use cases. It would be especially unfortunate if alternatives were available that do not have this awkwardness. That is the case here so the method doesn’t seem like a good idea to me.
I referred to orEmpty in the post. I think it is almost, but not quite, as good as the optional chaining solution, and would be my second choice.
It is clearly inferior though:
A projection runs the risk of not being optimized away properly. A manual if let wrapping the for loop never runs this risk. I am guessing the actual code generated for the optional chaining solution is more like the latter than the former.
It composes poorly with optional chaining: maybeRange?.reversed().orEmpty doesn't compile for reasons that are hard for users to understand.
It is inconsistent. Optional chaining of methods is just a flatMap, why not solve that in the library too? We have optional chaining for function/method/subscript invocation, and for assigning through optionals. Optional chaining composes well with while. Optional chaining doing nothing for for is the odd one out here.
There is also a mismatch between for x in a! but for x in a.orEmpty. Elsewhere, ! means trap on nil and ? means no-op on nil, and it should here too.
I don't buy that orEmpty is more discoverable. I don't think developers, faced with a compilation error because they are trying to for an optional, are going to look at the methods available on Optional for a solution. They are going to do what they know: unwrap it with an if let, or coalesce it. The way to drive discovery of this feature, either way we solve it, like you say, is with a fixit.
I also don't feel that orEmpty is clearer. I don't think anyone familiar with optional chaining and Swift in general won't guess what for x in d[k]? means when they see it. orEmpty doesn't even say what it does – we don't use the "or" idiom anywhere else in Swift.
@anthonylatsis Are you sure your nil coalescing pattern match is correct? I found several matches for “?? ” in the Swift repo when I was searching my various codebases.
When not mobile I hope to post my results. But I am mildly in favor of “for x in y?”. There needs to be an explicit indicator that the control flow may be skipped.
I've expanded my table with some open source Apple projects.
It's correct, but might not be covering as much cases as yours. Mind sharing the regex and searching tool so I can try it out?
While sorting through matches I realized that I had missed an important fact when I was advocating degenerate optional patterns. I use sequence? to exemplify, but forget that optional expressions come in all sorts, including those with keywords, like try? foo(...) or some as? Another. This really complicates things if we want to avoid parenthesizing, because every type of expression would need some kind of syntax, while the majority already express optionality pretty well on their own. So I guess this turns the scales for me in favor of optional chaining.
This argument could be made against any algorithm, I don't consider this to be compelling.
I find this to be a very compelling point, and I didn't think about that. Thanks!
There are lots of reasons, I don't find this argument compelling.
? certainly does not mean "noop on nil", it is a monadic operator that performs an operation when non-nil and wraps the result in an optional, propagating a nil if present.
I think it's problematic to make arguments based on comparison with other features, so just to get a feeling for the numbers, here's how many "repeat {" I found in the compatibility suite:
Thanks to everyone who participated in the review discussion. The formal review period ended yesterday. However, if you have any additional review feedback to give, or want to continue the ongoing side discussions in this thread, you are welcome to do so, until the core team makes its final decision regarding this proposal.
Will the (degenerate) optional chain solution make each of these work?
for e in try? s?.throwingAndReturningOptSeq()? { … }
for e in try? s?.throwingAndReturningOptAny()? as? [Int] { … }
for e in try? s?.throwingAndReturningOptAny() as? [Int] { … }
Since each !-counterpart works today (click for demo).
struct S {
func throwingAndReturningOptSeq() throws -> [Int]? { return [1, 2, 3] }
func throwingAndReturningOptAny() throws -> Any? { return [1, 2, 3] }
}
let s: S? = S()
// All these work:
for e in try! s!.throwingAndReturningOptSeq()! { print(e) }
for e in try! s!.throwingAndReturningOptAny()! as! [Int] { print(e) }
for e in try! s!.throwingAndReturningOptAny() as! [Int] { print(e) }
I agree that this is a problem for the .orEmpty() solution, but more importantly this also highlights a general "problem" of optional chaining; the flip side of its convenience:
In order to use optional chaining and never be surprised, the user must never forget that at each "link" in the chain, there are two types, the outer/resulting type and the type at the "link" / the user's cursor.
In regular chaining, there's only one type, the resulting type is always the same as the type at the user's cursor (which is also true for "force-unwrapped chaining").
The convenience of optional chaining is achieved by hiding its "nestedness" from the syntax and thus the user.
Optional chaining is nice, but it's important to remember that conveniences achieved via "white lies" have a price. While making some situations easier, they can also blur core concepts and introduce common misconceptions, which makes it hard to form an effective/correct intuition about the language.
It should never be hard for users to understand why their code doesn't compile, but if its hard to understand why maybeRange?.reversed().orEmpty() doesn't compile, then it's because it's hard to understand optional chaining, which is a (big) problem.
Thanks, I'm asking because all the three examples do currently work if you replace the ? with a ! (see code in the post), and I was under the impression that one of the goals was to make ? and ! analogous.
I was only considering it for looping at the moment. Making it fully equivalent has much farther ranging implications. Personally I’m not sure it would make sense, but I haven’t really considered it fully.
The trap you’ve pointed out is the most compelling reason I can see to explore solutions, but this proposal solves the wrong problem, because the trap will still be there in some other form. Fixing the way shadowing works in the language seems like a much more urgent problem, and one that we may not be able to solve later.
public struct FlattenedOptionalSequence<Base: Sequence>: IteratorProtocol, Sequence {
let baseNext: () -> Base.Element?
init(_ optionalSequence: Optional<Base>) {
switch optionalSequence {
case .none: baseNext = { return nil }
case .some(let s):
var baseIterator = s.makeIterator()
baseNext = { return baseIterator.next() }
}
}
public func makeIterator() -> FlattenedOptionalSequence<Base> { return self }
public mutating func next() -> Base.Element? { return baseNext() }
}
struct Empty { }
let empty = Empty()
func ??<S: Sequence>(lhs: S?, rhs: Empty) -> FlattenedOptionalSequence<S> {
return FlattenedOptionalSequence(lhs)
}
let maybeRange: Range<Int>? = 0 ..< 3
for e in maybeRange?.reversed() ?? empty {
print(e) // prints 2 1 0
}
let maybeHugeRange: Range<Int>? = 0..<Int.max
for i in maybeHugeRange?.reversed() ?? empty {
print(i) // prints 9223372036854775806 and breaks, no doom
break
}
that works for any sequence and has no problems with:
let maybeRange: Range<Int>? = 0 ..< 3
for e in maybeRange?.reversed() ?? empty {
print(e) // prints 2 1 0
}
or:
let maybeHugeRange: Range<Int>? = 0..<Int.max
for i in maybeHugeRange?.reversed() ?? empty {
print(i) // prints 9223372036854775806 and breaks, no doom
break
}
My tests didn't show any performance overhead compared to having the loop inside an if let.
Because orEmpty was proposed as a method on Optional so it is not available as part of an optional chain (unless a double optional is involved). Parentheses would be required: (maybeRange?.reversed()).orEmpty.