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

The review of SE-0230 — Flatten nested optionals resulting from try? begins now and runs through October 8th, 2018.

Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to the review manager (via email or direct message in the Swift forums).

What goes into a review of a proposal?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift.

When reviewing a proposal, here are some questions to consider:

  • What is your evaluation of the proposal?

  • 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?

  • 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?

As always, thank you for helping to make Swift a better language.

John McCall
Review Manager

8 Likes

What is your evaluation of the proposal?

-1

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

Possible - but not such a simple change...
I don't consider nested Optionals to be particular useful, but they are part of the language, and the proposal is fighting symptoms rather than cause (at least no one tried counter this argument so far).

Removing nested Optionals completely is most likely to source breaking to be considered, but the situation addressed by the proposal shouldn't be common anyways, so I don't think the churn it introduces is justified either.

The behavior of try? might not always be the most convenient, but it's simple and straightforward.
On the other hand, the change adds complexity:
Ask yourself what will happen when try? is applied to something that already returns something like Int??.
Right now, the easy answer is Int???

How much effort did you put into your review? A glance, a quick reading, or an in-depth study?
Took part in the original discussion (and afair read all posts there)

2 Likes

+1
This problem is significant.
Fits well with the language. It will remove some pain points.
The nesting optionality of Swift is something I have only used in swift. It is a weird feature that in my experience has only given me problems. I do not recount a single instance where in my code I ever wanted nested optionals. Dictionaries is another area where nested optionals are not very useful.

I greatly desire a generic level feature where I can specify that I only ever want one level of optionality.

The progressive improvement in this area is something that I welcome. I would like to hear from the core team on any plans for explicitly being able to enforce optionals with no optional nested values.

I read the proposal and followed previous discussions lightly.

3 Likes
  • What is your evaluation of the proposal?

I'm okay with this change.

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

I think we should make this breaking change since it aligns the behavior of keywords that make use of the ? for mapping a type into an optional. As described in the proposal the as? infix operator will technically try to flatten the type as much as possible (it could be achieved by calling flatMap on the optional type, but the concrete implementation might look different). In that sense there is no easy way to express the same flattening behavior for try? except of a manual one if let doubleOptional = try? something, let unwrapped = doubleOptional.flatMap({ $0 }) or a little bit more terse if case let unwrapped?? = something.

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

I personally say it does.

  • 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 quickly read the proposal.

What is your evaluation of the proposal?

+0.5
I think consistency between try? and as? is reasonable, and this is a decent solution to convoluted code that can be difficult to read.

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

I’m not 100% sure. Does this pass the bar of “actively harmful”? I’m not sure it does, though it is surprising to find such a glaring inconsistency not caused by API but by the language itself. I would put this in a “would be nice” group. Not essential, but welcome.

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

Yes, consistency is important in how our operators work, to make Swift an easy to learn and intuitive language.

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.

This precise use case is actually addressed in the proposal, but the underlying principle may not be clear. Perhaps this is a better way to explain it:

  • Applying try? to a non-Optional expression of type T wraps the type in Optional<T>.
  • Applying try? to an Optional expression does not change the type of the expression at all.

Or an even simpler formulation:

  • Adding try? makes an expression Optional, if it was not already Optional.

So applying try? to something that is already Int?? would not change the type at all. It would remain Int??.

5 Likes

I might have overlooked it, but the behavior wouldn‘t be consistent that way.

let a = trippleOptional() as? T // will flatten from T??? to T?

let b = try? trippleOptionalWithError() // will flatten only once from T???? to T???

type(of: a) != type(of: b)

Casting doesn't really "flatten"; it looks for any way in which it can interpret the operand value as the target type, and then wraps in a level of optionality to represent that that search can fail. But it'll happily look through protocol types and so on. Any static behavior is going to seem inconsistent compared to that if you think of it as flattening, including the current behavior of try?.

5 Likes

The proposed behavior of try? is more similar to performing as? <subExpressionType>. So instead of comparing to as? T, it would be more similar to as? T???

func tripleOptional() -> Int??? {}
func throwingTripleOptional() throws -> Int??? {}

let a = tripleOptional() as? Int???   // produces Int???
let b = try? throwingTripleOptional() // produces Int???

This actually produces four levels in 4.2

I am totally confused about the return on investment here. This is not what I understood from the proposal. Either the proposal needs to make this more clear or the name needs to change away from calling this a ‘flatten’ behavior. Shaving only one level off will make this addition soooo confusing.

I am leanning towards a more holistic approach instead if that is the case.

1 Like

I am not sure what is the benefit from the user point of view of not flattening always to a single optional... Int????? does fill the abstract rules we have today, but from a user point of view having a single level of optionality max makes most sense. The language and tooling should make it possible to stay always at the Int? level and avoid Int?? or Int??? at least from the user visible point of view (regardless of the internal behaviour / machinery).

1 Like

As far as I understand it, it's more or less the same what optional chaining does (but something completely different than as? - I think the proposal is just plain wrong in this respect).
Full (or maybe it would be better called "maximal") flattening would be without precedent, and imho it would make everything even more complex.
I tried hard to start a discussion about the general utility of nested Optionals in the original thread, but I guess debates about issues that won't be addressed aren't that popular ;-)

1 Like

Nested optionals seem to me as an implementation detail leaked out to users. What is the advantage of user level code thinking about Int?? vs Int?... something that could contain something that could contain something...

3 Likes

What is your evaluation of the proposal?

+1

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

I never find any use case I really need Int???? over than a simple Int? from the daily development.
The current behavior of try? makes optional chaining less intuitive than it should be.

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

I totally agree that nested optionals are difficult for users to reason about when using try?.
The proposed solution does improve the experience and make the language much easier to learn.

So I think it fits the direction of Swift.

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

Quickly read the proposal.

Edit: Modify my review that may confuse the goal of this proposal.

I wouldn't say a flat "wrong", but I agree that comparing try? with as?, when done too fast, can bring confusion.

As @John_McCall says above, as? is of its own kind.

The proposal is not, in all rigor, about bringing try? closer to as? on optional depth. But it does indeed, in practice, almost by chance, bring try? closer to as? on the number of concrete types actually found in a given program.

The real motivation, for me, is that the Swift language actively avoids adding levels of optional nesting unless explicitly required by the user. T?? exists, but we all know it is confusing, so you have to ask for it.

  • What is your evaluation of the proposal?

+1

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

Yes, it is a significant problem.

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

Yes.

  • 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?

Read the proposal and previous comments.

… Huh. That's true. Okay, I agree that optional chaining is a better model for what is proposed here than as?.

The point of the proposal is to avoid avoid adding any additional levels of Optional-ness when using try?, other than the minimum required. If your type system already has nested Optionals for some other reason, try? should not be the tool for removing them.

This proposal just enables us to avoid creating a nested Optional unless the user explicitly requests it.

5 Likes

We appreaciate the work that went into this. I just read this sentence three times. I still interpret this a single level flattering. I know that that is not what you’ve meant.

Other people seem to also think of this proposal as total flattening.

I’ve realize that even saying something like “single level flattening” is ambiguous. What does that mean? Taking a level off if there are more than one? Or does it mean alwayss producing a single level of optionality?

I do not believe people are reviewing the same proposal. I would kindly ask for the proposal to be sent back to revision.

+1.

Yes. I remember the first time I ran into this rough edge, and thinking that it was definitely the incorrect choice. I think the particular case dealt with optional chaining a throwing function: try? someOptionalValue?.someThrowingFuction() returns a double optional. I was confused when it caused my code not to compile, and when I figured out what was going on, I was upset — of course I didn't want a double optional! Why would I want a double optional? I'm trying to elide the error, not still have to deal with it.

Yes. As the proposal notes, it's similar to how as? works. It was quite a while into my Swift experience where I realized that as? handles both optional and non-optional values on the LHS, and I remember being pleasantly surprised at how the feature does the right thing and I didn't have to think about it. try? currently doesn't have that delight.

Only slightly related: Kotlin has optional chaining that doesn't consume the entire rest of the expression, only the next method call. Where in Swift you might have let result = optionalValue?.nonOptional.nonOptional.nonOptional, in Kotlin, you have to insert intermediate ? for each method call: let result = optional?.nonOptional?.nonOptional?.nonOptional. It's not exactly the same situation, but I do think it shows that when doing the exact technically right thing (Kotlin's behavior here) feels totally wrong in most situations, you should do thing that meets the users of the language where they are.

Quick reading.

3 Likes

No, I don't think the goal of this proposal is to totally flatten optionals.
That's just my personal preference to prefer single level optional operation.
If there was really a use case that nested optionals make sense. I'm fine to have them.
So it shouldn't be the reason to block the main idea of this proposal.

I edited my review to bring the focus back to the original idea of this proposal.