SE-0230: Flatten nested optionals resulting from `try?`

  • What is your evaluation of the proposal?

I'm a little surprised we're considering this at all since it's a source compatibility break. But assuming we've cleared that hurdle, I think it's probably a good idea. While it's less pedantically correct, I think users will find it more useful in practice. try? is a convenience feature—it ought to be convenient.

  • Is the problem being addressed significant enough to warrant a change to Swift?

It does cause much confusion to some users, and the source compatibility suite analysis suggests that most users are working around the current behavior, not using it. So I'd say yes.

  • Does this proposal fit well with the feel and direction of Swift?

Yes. This change matches optional chaining's behavior.

  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

N/A.

  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

Quick reading.

4 Likes
  • What is your evaluation of the proposal?

+1. I think it's unfortunate that this is addressed so late, but Swift will be a better language for it by not having

try? JSONSerialization.jsonObject(with: data) as? [String: Any]

produce a double optional.

  • Is the problem being addressed significant enough to warrant a change to Swift?

Yes. try?'s current behaviour needlessly causes confusion for many developers. A common way people currently work around this problem is with

(try? JSONSerialization.jsonObject(with: data)) as? [String: Any]

which won't break, so I don't expect a lot of code to break because of this change.

  • Does this proposal fit well with the feel and direction of Swift?

Yes. Like Brent said, this is consistent with how optional chaining works.

  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

I read the proposal and followed the earlier discussions.

2 Likes

What is your evaluation of the proposal?

+1

Is the problem being addressed significant enough to warrant a change to Swift?

Absolutely. The existing behavior is highly unintuitive and causes a break in development flow even for experienced Swift programmers. I've run into the issue this proposal addresses many times myself and never once have I hit a case where the existing behavior was desirable versus the proposed behavior.

Does this proposal fit well with the feel and direction of Swift?

Very much so.

If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

N/A

How much effort did you put into your review? A glance, a quick reading, or an in-depth study

Somewhere between a quick reading and an in-depth study.

1 Like

My position is basically the same as @beccadax's.

What is your evaluation of the proposal?
+1

Is the problem being addressed significant enough to warrant a change to Swift?
Yes. I have encountered this several times, and it’s always painful to work around it, resulting in distracting extra ‘boilerplate’ code.

Does this proposal fit well with the feel and direction of Swift?
Yes. It’s pragmatic and ergonomic, and fits the general pleasantness of Swift.

If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
N/A

How much effort did you put into your review? A glance, a quick reading, or an in-depth study?
Quick reading after following the previous discussion.

1 Like

What is your evaluation of the proposal?

I was against this proposal until I realized that as? casting exhibits flattening behavior. Now I'm strongly for it. It solves a very unfortunate ergonomics issue and I see no problems with the proposed solution.

From the source compatibility analysis, it seems like (try? getXYZ()) as? XYZ is the simplest workaround people have discovered and the proposed solution is basically compiler sugar for exactly that. It's simple enough to describe and should quickly become second nature. :+1::+1:

Is the problem being addressed significant enough to warrant a change to Swift?

The issue is minor, but it sounds like the impact will be minor (and mostly positive to me) based on the compatibility analysis. Making the type system more ergonomic can only help to persuade users of typeless languages to consider using Swift — and that is a big deal for we backend developers.

How much effort did you put into your review?

Read the proposal, discussion, and experimented with some of my own sample code.

2 Likes

What is your evaluation of the proposal?
+1

Is the problem being addressed significant enough to warrant a change to Swift?
I think it is.

How much effort did you put into your review? A glance, a quick reading, or an in-depth study?
Quick reading and personal experience with nested optionals.

I honestly still don't know what to think about this proposal, despite participating in the discussion thread and thinking about it extensively. On the one hand, this makes the behaviour of try? significantly more complicated (as seen several times in this very review thread) and I would generally prefer simpler rules with a little more boilerplate. On the other hand, the behaviour of all these other related language features (e.g. as?) is already similarly complicated and hard to internalise (again, as seen in this review thread), so perhaps making try? slightly more ergonomic in some cases is worthwhile.

To summarise my thinking, the downsides of making this change are breaking source compatibility and complicating the behaviour of try?. The upsides are improving consistency with other related features and avoiding the double optional, which can be surprising and require some extra code to unwrap. I will note that the analysis of the source compatibility suite for source-breaks also double as a rough benefit analysis for this change, and the result is that it possibly helps in about 1-2% of try? uses.

1 Like

Hello

  • What is your evaluation of the proposal?

A useful changes

  • Is the problem being addressed significant enough to warrant a change to Swift?

Yes, there some inconsistencies in ?-related operator behaviours.

  • Does this proposal fit well with the feel and direction of Swift?

Yes, we should lead Swift to more uniform optionals behaviour.

  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

Quick reading

What is your evaluation of the proposal?

+1

Is the problem being addressed significant enough to warrant a change to Swift?

Even though the proposed solution introduces a source breaking change I think it warrants a significant improvement for all Swift users. After teaching many students the basic of Swift most of them were confused once they encountered this problem the first time as it doesn't behave like as?. Furthermore, I think even experienced users get a great benefit out of this and it improves the language.

Does this proposal fit well with the feel and direction of Swift?

Absolutely

If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

N/A

How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

Followed the discussion thread and read the proposal

1 Like

I'm mostly for this proposal. I think this breaking change is okay for a couple reasons:

  1. Double optionals are mostly uncommon in natural Swift code that doesn't arise from language semantics.
  2. try? is just a sugar, and having it create double optionals when most of the time the user is going to be unconcerned with the levels of optionality and what they entail is a bit much.

My main hesitations are around changing semantics of a construct this late in the game. I don't think this optional wrapping behavior is actively harmful. While things will get wrapped in double optionals, this behavior is well defined and safe. There's no risk of the user trying to use the value when it could be nil, since Swift still protects you here.

1 Like
  • What is your evaluation of the proposal?
    +1
  • Is the problem being addressed significant enough to warrant a change to Swift?
    Yes. This has been the most annoying thing (syntactically) and currently works in a non-intuitive way compared to most other use cases.
  • Does this proposal fit well with the feel and direction of Swift?
    Yes. Making error handling easier and less verbose is good for the future usability of the feature
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?
    Read through it and gave it a few days of thought.

My only suggestion would possibly be to add a compiler warning when returning a func() throws -> \*Type\*? that explains the behavior. Trying to think of how to phrase the warning makes me think there's an underlying issue with the complexity of wrapped optionals as others have pointed out.

Strong +1

Agree with this good quote from @beccadax:

It's good to see analysis in the proposal about how this will affect source compatibility. Seems like a non-issue for almost all cases.

1 Like
  • What is your evaluation of the proposal?

+1

  • Is the problem being addressed significant enough to warrant a change to Swift?

I believe so. try? is a convenience feature that is at times inconvenient to use. This proposal addresses that deficiency.

  • Does this proposal fit well with the feel and direction of Swift?

Yes. Ergonomics is important.

  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

n/a

  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

I participated in the original discussion.

What is your evaluation of the proposal?

+1, after some chin-stroking.

Is the problem being addressed significant enough to warrant a change to Swift?
Does this proposal fit well with the feel and direction of Swift?

I think so. I was a bit skeptical on first blush, but two things convinced me:

  1. The research on the source compat suite's non-reliance on the existing behavior is compelling.
  2. The parallel to as? is decisive.

When Swift is at its best, it feels simpler than it truly is because it provides a sort of heuristic consistency that allows developers to reason about disparate features using a unified mental model. (This is closely related to the idea of “progressive disclosure” that Chris Lattner talks about.)

When Swift is at its worst, it feels like a minefield of special cases because it forces developers to understand its subtle formal distinctions in ways that prevent a casual mental model from being effective.

The former should be an ongoing design goal for Swift — and the language should be willing to tolerate some judicious source breakage to support it.

If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

Swift’s type promotion/conversion conveniences around Optional make it far more pleasant to use than the otherwise nearly identical Maybe types in ML family languages. I second Brent: conveniences should be convenient. And this convenience is one of Swift’s defining features.

How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

Quick glance.

7 Likes

I have updated the proposal slightly to clarify how the proposed change compares to the behavior of as?. In short, because as? takes an explicit type, it can flatten or add multiple levels of optionals if requested. That is not proposed for try? here. In practice, though, as? is often used to flatten two optionals down to one, as in the JSONSerialization example.

The parallel to the behavior of optional chaining (foo?.bar) is unchanged.

The updated proposal can still be found at the original link. If you're interested to see what changed, here is the diff.

5 Likes

Not only that, but 'as?' considers the dynamic type of the left-hand side (and indeed that is the entire point of the operator), whereas the operator here very much does not, as you make clear in the section about generics. For that reason, the comparison with 'as?' here is misleading, in my opinion.

I think this proposal is a good example of one that would benefit from a "How do we teach this?" section that was discussed in another conversation. Here, the claim is made that the proposed change is more intuitive and consistent with the rest of the language than the current state of affairs. A crucial demonstration of that claim would be showing how teachable the new rules are. However, if readers of this proposal approve of it on the misunderstanding that the operator 'try?' will be aligned with the operator 'as?', then we will have done the cause of intuitiveness and teachability rather a disservice.

That said, because the rules proposed here more closely align 'try?' with '?.' chaining, I would have been in favor of them unreservedly if we were still in the Swift 3 (iOS dictation: swiffer three) timeframe.

My major hangup as to this proposal overall is largely the same as those expressed by others, namely that it is source breaking at this late stage, and I am not entirely convinced that the benefits going forward can justify outright source breakage for Swift 5. Because, after all, this all has to do with static typechecking, misunderstandings are largely flagged by the type checker at compile time, preventing such misunderstandings from being actively harmful to the correct behavior of the resulting code.

I see it the other way. To-date, I haven't seen any arguments for the current behavior in terms of logic. Even those who argue that information will be lost if this change happens cannot show any examples where anyone cares about that information. Rather than being too late to make this change, I see this as the last opportunity to make the behavior match what developers actually want from the operator.

7 Likes

Well, as I said here and in the pitch thread, the behaviour of try? is much simpler and (at least for me) easier to understand if it always introduces one level of optionality. And I think the confusion throughout this thread about the behaviour of other similar constructs shows that there might be a benefit to having simple behaviour. That said, I still don't have a strong opinion either way on the proposal. It comes down to a marginal cost-benefit tradeoff, and it seems rarely useful or harmful either way.

2 Likes

I think I agree; the analogy with as? in the proposal is misleading. It was included because of a misunderstanding I had about how as? works when I wrote the proposal. I’ll update the proposal tomorrow to just remove the comparisons to as? entirely (other than those used incidentally, as in JSONSerialization examples).

However, the analogy to optional chaining seems solid to me. In regard to “how do we teach this?”, I think we can do so the same way the Swift Language Guide explains optional chaining:

  • If the type you are trying to retrieve is not optional, it will become optional because of the optional chaining.
  • If the type you are trying to retrieve is already optional, it will not become more optional because of the chaining.

Therefore:

  • If you try to retrieve an Int value through optional chaining, an Int? is always returned, no matter how many levels of chaining are used.
  • Similarly, if you try to retrieve an Int? value through optional chaining, an Int? is always returned, no matter how many levels of chaining are used.

This explanation can be used almost verbatim to describe the proposed behavior for try?. That suggests to me that this proposal is well-aligned with what users might expect as they learn the language.

As to concerns about source compatibility, I would be interested to hear of any cases where this would be a source-breaking change. There do not appear to be any in the compatibility suite. However, the compatibility suite is biased toward libraries rather than full apps, and it may be the case that libraries are more likely to expose error conditions, and thus less likely to use try? in the first place.

In the apps I work on professionally, this change would not be source-breaking; all of our uses of try? remain compatible under this change. But if such breakage is out there, I’d love to know what it looks like so that we can make sure it is migratable if this proposal is accepted, and so that we can get a sense of how much incompatibility exists.

4 Likes