[Pitch] Extending optional chains to include for loops

This is about optional chains used as the "sequence expression" in a for-in loop.

After some recent discussion in another topic, I tried to start to start a more formal pitch there:

Please note:

  • This is just my (humble) try for a first text, a better text might dispense with most of its contents.
  • The pitch is incomplete insofar as the detailed design (grammar?) and then also the implementation is missing.
  • I tried to include all known objections to the proposed solution.
  • I used an external library as use case, but I think in a very understandable way, and there is a real use case behind it.
3 Likes

I think this needs a much stronger motivation. I cannot see the need for this, based on the examples provided, and I disagree that the suggested syntax improve clarity in the examples given.

1 Like

That would also be an argument against some other usages of optional chaining.

1 Like

While that might be true, there is always a much higher bar for language changes, especially if they are purely syntactical sugar as in this case, than there is for keeping the status quo for feature that have been part of the language for a long time already.

That‘s a sensible strategy on one hand, but on the other hand making the language feel consistent should also be an important point. “We have this great feature of optional chaining, but why can’t I use it at this place?” could be the reaction of someone who is less involved in some strategy for the language but just uses it. It is not about inventing a new language feature per se.

1 Like

Note that the pitch text has been updated to take some of the above discussion into account.

Maybe that goes for some usages of optional chaining. Like optional closure execution someClosure?(), but in general I think optional chaining is perfectly reasonable.

Sure, optionality is viral in the chain, and may be easy to miss sometimes. But at the point of unwrapping, it is usually very easy to see exactly what is going on, and there are lots of visual clues that a value may be nil, like .e.g indentation. However, for this pitched feature, I think it is very easy to miss.

(For exactly this reason, I would have liked for someClosure?() to warn that "Result of call to function returning 'Void?' is unused", but I guess that train has left the station)

Could you please give an example where the optionality could be easy to miss? (This is not to downplay your concern, I am really interested in those examples.) Thanks!

1 Like

All of them. IMHO the feature is undesirable because the single ? sigil does not have not enough visual weight to see that the nil-case is an implicit no-op. Even worse in optional chains without a trailing ?. Combined with what I see as a very weak motivation, the benefit does not justify the cost.

I realize that you obviously consider the cost lower and the benefit greater that I do, but as it stands, I fail to see a compelling motivation.

If this pitch is successful, I will have to impose linter rules in my organization to disallow optional iteration.

Then it would be interesting to know why this is such a huge problem for for-in loops but obviously not for all (?) usages of optional chaining. I guess you then also would not like to use forEach in your organisation as there you can already use optional chaining.

Sorry I do not want to bother you with those questions, I am really interested. No need to answer.

The pitch text has again been updated.

As I said, it is also problematic when function evaluation is at the end of the chain, and that I wish such call sites would complain that the result is unused. E.g. someOptionalCollection?.forEach { ... } returns nil if the the collection is nil, and .some(()) if the collection is non-nil, so there is a return value which is silently discarded and obfuscates the fact that there is a no-op branch in there.

However, most use cases for optional chaining is to walk a property path, evaluate each node, and return the leaf node wrapped in an optional. That is, we still have an optional.

The result of optional chaining is always an Optional, and using it almost always requires the usual ceremony where optional values are involved. That is, binding it, unwrapping it, guarding on it, pattern matching it, or whatever. This ceremony is usually very visible and easy to read:

if let foo = some.long?.winded.path?.of.optionals {
  // it's pretty obvious that we're dealing with branching here
}

But it still removes the clutter of unwrapping each node along the path:

if let long = some.long, let path = long.winded.path, let foo = path.of.optionals {
  // the branching is here is obvious as well, but the clutter doesn't add much in terms of clarity
}

I don't think all optional chains are bad.

However, the branching and implicit no-op here is obfuscated at a glance:

for item in some.long?.winded.path?.of.optionals {
  // it's clear that this is a loop, but not that it is wrapped 
  // in an implicit if-branch, with a no-op path
}

I don't think you can convince me otherwise. You can perhaps convince me that the feature is worth the cost, if the motivation is compelling enough, but so far I don't see it. Either wrapping the iteration in an if-let, or appending ?? [], or creating an extension on Optional where Wrapped: Sequence that returns some kind of OptionalIterationSequence<Wrapped.Element> or whatever, is probably a better solution, IMHO.

I think I have voiced my concern with this pitch, and I don't think it is fruitful for the discussion to spend much more time on me. Either let the discussion progress with other aspects of the pitch, or address my concerns with better motivations.

3 Likes

I tried to formulate the concern in the pitch. Thank you.

I just would like to note that currently the pitch draft is in some kind of hibernation:

  • The “Detailed design” section is still missing, and I cannot deliver that, I am not able to provide an implementation.
  • Support for the described idea by the community seems to be limited.

There seem to be quite opposite opinions about the proposed feature. For me, without this feature “for-in” loops are kind of unusable in many cases where I need some kind of “for” loop, while for others the usage of optional chaining in a “for-in” loop is too easily overlooked.

At this point there will not be any further action on my part. I tried to include all known objections into the pitch draft, so it should be a good overview over the pros, the cons, and the alternatives. I will leave the pitch draft available at the current URL for a long time.

I think this problem definitely merits addressing – optional collections do happen in the real world and are a real pain when they aren't expressible by array literals – and I think the proposed syntax would be a good solution.

However, I think the proposal could do with giving more serious consideration to the alternative of a orEmpty property on Optional<Sequence>, as Joe described in his rejection of SE-0231.

The reasons for not considering it in this proposal are not compelling to me:

  • Preserving symmetry with forEach: forEach is a bug not a feature (IMO at least :)
  • Discoverability: if .orEmpty is in the std lib, the compiler can produce a fixit to use it when attempting to loop over an optional.

Here are the pros/cons of the two options as I see them:

Pro for x in y?:

  • follows existing practice of sugaring optional chaining with ?
  • very succinct while (arguably) clear given the existing language conventions
  • potentially even more succinct if "chained" e.g. for x in try? y does not require parens unlike for x in (try? f()).orEmpty which is pretty noisy

Pro for for x in y.orEmpty:

  • easily searchable
  • can be implemented in the library today without compiler magic
  • (arguably) clearer
  • discoverable via completion (tenuous, especially if the fixit idea is pursued)
Implementation of orEmpty if you want to play with it
struct MaybeSequence<Wrapped: Sequence> {
  let wrapped: Wrapped?
}

extension MaybeSequence {
  struct Iterator { var wrapped: Wrapped.Iterator? }
}

extension MaybeSequence.Iterator: IteratorProtocol {
  mutating func next() -> Wrapped.Element? { wrapped?.next() }
}

extension MaybeSequence: Sequence {
  func makeIterator() -> Iterator { .init(wrapped: wrapped?.makeIterator()) }
}

extension Optional where Wrapped: Sequence {
  var orEmpty: MaybeSequence<Wrapped> { .init(wrapped: self) }
}
5 Likes

By your own logic, .forEach should not exist because for loops exist, despite sometimes requiring a substantial refactoring to switch. I think it is only consistent to argue that .orEmpty is similarly duplicative of first pattern matching away the optionality or using an inline ternary operator.

The argument in favor of .orEmpty also extends to things like Rust’s Result::is_ok. By that logic, Swift should have Result.isError. I don’t necessarily disagree.

I think the question mark — despite being short — is better recognizable as a “warning” that we are kind of iterating through an optional sequence than “orEmpty”, which looks like some normal property. The question mark is what we are looking for to discover optional things so to speak.

“orEmpty” is Alternative 4 in the pitch draft. I will change the name of the property accordingly and also list the pro-points that @Ben_Cohen mentioned (tomorrow, I will then add an according note in this topic).

BTW I have no problem if someone would like to rewrite the pitch draft (switching the author is often a good idea). But we should not loose any of the arguments or alternatives.

Well, this then is a pro-point in favor of this pitch I would say.

Could we do this (perhaps with language support)?

    for x in y ?? .empty {
    }

where empty is an "empty sequence of appropriate type".

We would probably allow it in other places as well:

func foo(_ array: [Int], _ dictionary: [Int: Int]) {...}
foo(.empty, .empty)
1 Like

This looks pretty good to me, and should be implementable in today’s Swift.

One difficulty would be defining CollectionOfOne.empty.

1 Like

I would not like to use this because, as I mentioned before, I use those optional chains extensively in iterations, so I see this formulation as a burden that to some degree destroys the conciseness of my code. But I will add this alternative to the pitch draft.