That sounds like an entirely different proposal. I think I agree with this thought.
We're not going to remove nested optionals from Swift at this point.
Fine, will be mature about it...
he proceeded to kick, scream, and throw toys out of pram until someone pacified him with Joe Groff’s Dad/Nerd Jokes Greatest Hits Collection (narrated by Brian Blessed)
Seriously though, is there a (perhaps historic) reason for user level code to have ever had to be exposed to nested optionals / reason with nested optionals in terms of benefit for user level code?
Beside implementation (compiler, stdlib, etc...) details, why have we not always flattened optionals to a single level?
I might be missing something obvious, sorry for being thick :), hence my questions.
Kind Regards,
Goffredo
Double optionals happen, and are useful, for example, when you have a dictionary of optional values:
let dict: [String, Int?] = ...
// dict["foo"] is Int??
if let int = dict["foo"] {
// "foo" is in the dict
if let int = int {
// value for "foo" is an Int
} else {
// value for "foo" is nil
}
} else {
// "foo" is not in the dict
}
A use case could be a cache for resources that are heavy to grab, and potentially missing:
let fileContentCache: [URL: Data?]
I thought so ;-) - but still, imho the question remains weather this proposal is really about improving try?
or working around a much more general issue:
Afaics, the arguments for it can be applied against any use of nested Optional
s, so why add an exception for try?
, and not for other cases (which haven't even been collected, let alone discussed)?
Double optionals are the natural result of treating Optional
like a normal type rather than an attribute of certain uses of a type. They also encode a distinct meaning in most places where they can occur. Some users in some circumstances don’t care to distinguish between Optional(nil)
and nil
, but the two almost always occur in different situations (like “there is no entry” vs. “there is an empty entry”).
The lack of a feature like double optionals is what forces Objective-C to include the NSNull
type, which makes a programmer’s life more complicated whenever it can occur and still doesn’t fully solve the problem.
Double optionals are very elegant and I wouldn’t remove them if I could—but source compatibility is the nail in the coffin. If they were fully eliminated from the language, some uses literally could not be fixed without major rewrites to avoid using standard library functionality. That isn’t an option. Even if we wanted to get rid of them, we can’t.
So it’s just not worth discussing. We can tweak double optional behavior in places where, like try?
, we’re pretty sure nobody uses them, but we have neither the desire nor the ability to completely abolish them. It’s not on the table.
I have seen this example before, but no other - and even without nested Optional
s, this could still be modeled (maybe even better):
dict.keys.contains
wouldn't need any comment to explain its meaning, and with optionals that are modeled as T | Nil
, it's possible to construct a dictionary whose return value is T | Nil | NoValue
.
Obviously, this ship has sailed long ago for Swift - but that is no reason to glorify nested Optional
s.
I've had it come up a few times in my code (usually when creating some sort of generic wrapper/container type), and it is important to have nested optionals in the language when they do come up.
That said, I have never wanted try?
to create a double optional. I am +1 for this proposal, since it brings try?
in line with the way it is actually used. As someone said earlier: Conveniences should be convenient.
I know it is technically source breaking, but I don't think it will actually break any real world source. In all of my code where this comes up, I have had to use the parenthesis in a way which makes this change have no effect (unless I remove the parenthesis). I have yet to see a real-world use case where code will actually break...
What is your evaluation of the proposal?
I approve.
Is the problem being addressed significant enough to warrant a change to Swift?
That is the tricky question since source-compatibility is valued pretty heavily at this point. When I use try?
I always flatten double optionals, and I'd be happy to see a reduction in the syntactic complexity to achieve this. But it'll be source-breaking in some places. Here's a concrete example from my code:
var urls: [URL] = recents.compactMap {
(try? $0.resolve(relativeTo: nil))??.url
}
This double-unwrapping ??.
optional chaining will no longer work if the second layer of optionality goes away, so I'll have to rewrite it like this:
var urls: [URL] = recents.compactMap {
try? $0.resolve(relativeTo: nil)?.url
}
I take note the compiler offers a fix-it when attempting more unwraps than there are layers of optionals, so in a situation like this fixing it is a no-brainer. Perhaps the migration assistant could do it for you.
And this is the only line that'll be affected in my code. I'm not worried and I'd be happy to make that line prettier.
Does this proposal fit well with the feel and direction of Swift?
It does make try?
more convenient, and more alike to optional chaining. I think it's more natural to flatten optionality when possible in most cases. Pragmatism should be the guiding principle for convenience features like this one.
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 the review thread.
This discussion about nested optionals in general has been interesting and informative, but it's off-topic for this review; please take it to a new thread if you'd like to continue the discussion.
- What is your evaluation of the proposal?
+1. I'll be brief as I don't have a lot to add in support and participated in the discussion.
Users intuitively expect optional sugar features to work together in exactly the manner that is proposed. Further, I have never encountered a case where try?
was used and this behavior is not desired. Lack of this feature reduces clarity in all code I have seen that uses try?
and results in a double-optional.
- Is the problem being addressed significant enough to warrant a change to Swift?
Yes. This is a small, but meaningful wart in Swift's optional sugar.
- 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?
I have not used any other languages that have pervasive syntactic sugar for optionals.
- How much effort did you put into your review? A glance, a quick reading, or an in-depth study?
I participated quite a bit in the discussion threads and gave the final proposal a quick read.
What is your evaluation of the proposal?
-1. This change would add another source of possible confusion related to Swift's imo already too special/magic optionals, making them (and their sugar) even harder to understand in terms of the rest of the language. It currently suffices to explain/understand this:
try? (...) -> T returns T?
but the proposal wants to change that to:
try? (...) -> T returns T?
unless
T is statically known to be Optional<U>
in which case it returns T
Is the problem being addressed significant enough to warrant a change to Swift?
The problem seems more related to the unintuitive precedence of try?
vs that of as?
, and I think solving the problem like this will only introduce more (other) problems (caused by the added complexity).
Does this proposal fit well with the feel and direction of Swift?
It might be, I'm afraid optionals are only becoming more and more of a major stumbling block¹ for newcomers by complicating rather than simplifying the rule set needed to fully understand them.
(¹) nil/.none, lots of cryptic sugar with complicated rule sets, Optional being an enum is just to be considered as an implementation detail and trying to understand it as such might be actively confusing your trying to understand it, etc) as a result of good intentions, ie trying to make them easier to use according to some people in some situations.
How much effort did you put into your review? A glance, a quick reading, or an in-depth study?
I first glanced the proposal and this thread, and then read the pitch thread, in which I found my issues with the proposal represented by many posters.
Two questions:
-
What would the documentation for
try?
look like after the change? I'm asking because I fear that it would have to be quite large in order to fully cover its behavior. -
Is there an answer to the parenthesized question in this quote from the pitch thread:
This proposal is changing try?
to use the same rule used by optional chaining. Could you clarify what you see as a new inconsistency?
I don't see enough similarity in function between try?
and optional chaining for them to be either consistent or inconsistent. All I'm saying is that the only thing I need to keep in my head to understand the current try?
is:
Well, they both try to do something and then return nil in an exceptional condition, so I see them as very similar.
Anyway, you don’t have to justify your review further; I was just curious about that point.
I guess my mental model -- which might be wrong -- of the current try?
and optional chaining is something like this:
-
try?
always adds one layer of optionality, as a representation of the success (.some) or failure (.none) of the error handling layer. So it simply converts the error handling layer into an optional layer. (I'd like to keep it that simple, and not have to remember that for some return types oftry?
's sub-expression, the optional layer will be skipped.) -
Optional chaining is a way of reaching into the wrapped value of an optional, always exactly one layer in. And within it, there can either be a value or not. It's just syntactic sugar for flatMap, isn't it? It is not possible to perform optional chaining on any other types than Optional, so it is not possible to introduce a special rule similar to the one that this proposal aims to introduce to
try?
.
Note that I'm happy to be corrected if I'm wrong. I'm only sharing my thoughts and review because I think my perspective as a non-expert can be interesting.
Optional chaining isn’t literally compiled into a call to flatMap
, no. For example, it can be used to call a mutating
method, or on the left-hand side of an assignment. It will also collapse multiple sources of optionality into a single optional if you write e.g. foo?.bar?.baz
.
The semantics are that it checks for nil and short-circuits, producing nil as the result.
It’s true that currently, try?
maps the error into an optional, so you can use the some/none
value to determine if there was an error. Under this proposal, that would not always be possible.
But this proposal argues that using try?
’s optional to check for an error is uncommon and non-optimal. If you want to check for an error, you’re likely to use try
, which is more suited to the task.
This proposal suggests that we align with what most developers are trying to do when using try?
, which is access the return value if possible, ignoring any errors.
So the change is that we move from focusing on the presence of an error, to focusing on the presence of a return value.
I think I finally see what people mean by making try?
more consistent with optional chaining now. And it can be exemplified like this:
struct S {
func o0() -> Int { return 123 }
func o1() -> Int? { return 123 }
func o2() -> Int?? { return 123 }
func o3() -> Int??? { return 123 }
}
print("Using non-optional S (and thus no optional chaining):")
let s: S = S()
print( type(of: s.o0()) ) // Int
print( type(of: s.o1()) ) // Int?
print( type(of: s.o2()) ) // Int??
print( type(of: s.o3()) ) // Int???
print("Using optional S and optional chaining:")
let os: S? = S()
print( type(of: os?.o0()) ) // Int?
print( type(of: os?.o1()) ) // Int? // I would have expected one more ? here
print( type(of: os?.o2()) ) // Int?? // and here
print( type(of: os?.o3()) ) // Int??? // and here
So try?
will be made more consistent with optional chaining by making it as inconsistent as optional chaining is.
I didn't know (until now) about this "inconsistency/special case" in the behavior of optional chaining, and I find it a bit weird/disturbing/hard to remember in the same way as the proposed change. I mean, why does optional chaining add zero or one level(s) of optionality? Why not always one? I assume this special rule is not applied in a generic context where T
cannot be statically known to be an Optional<U>
, similarly to how this proposal aims to solve the situation for when the type of try?
's sub-expression is a type parameter cannot be statically known to be or not be an optional.
I guess I don't understand why this should be baffling, especially if you rewrite it in a more user-friendly form:
try? (...) -> T? returns T?
try? (...) -> T returns T?
That makes the next line easy: check for a nil, otherwise you have a T.
But when that is actually this:
try? (...) -> U? returns U??
the next line is no longer easy. One unwrap is no longer enough, and the if
test is now multi-partite, and which unwrap means what, and do I care which is which, and … and … suddenly it's baffling.
If (as you said) you think optionals are a stumbling block to understanding, double optionals are IMO exponentially worse harder.