`if let` shorthand

That's a commendable attitude, and I've seen them quite a lot recently for some reason. Unfortunately, it's never that simple. Swift has never been in a feature rush*, where the first feature gets all the says, and everything else follows.

We have manifestos, guiding proposals forward and ensuring they fit together. The proposal template also has the Future Direction to see how it could further expand. The review template even asks whether it fits with the feel and direction of Swift. We can hardly view any proposal in isolation.

That is not to say that we should anticipate all possible variations of proposals yet to be thought of. Of course not, but it bears to keep in mind for directions that are extremely likely, especially one(s) that could clash with the proposal. It's not wise to dismiss that something because it has not been proposed. You'll also have a much easier time convincing people if you can show that the thing being brought up is either a) not extremely likely to happen, or b) not in direct conflict with the proposal, which is usually an easy bar to clear (not to comment on the current pitch and the new ownership model specifically).

* There are, of course, times when that is debatable or even controversial, but it's controversial precisely because that's not how we usually do things or what we perceive to be the right thing to do.

7 Likes

Allow me to clarify what I meant: the features don’t exist yet. The proposals do.

That just gives you more leeway to show a. (in addition to b.), no? ;)

Only from curiosity:
Isn't this (repeated) proposal only a symptom of the missing flow-sensitive typing, is it?

Changing this typing sounds not simple, but very complex, I guess :)

Regarding flow-sensitive typing, see up thread: `if let` shorthand - #62 by Ben_Cohen

2 Likes

Mostly, people caveat some of their opinions with "I think" or "It seems". I do so liberally in this thread. But it becomes very tedious to read if you do it constantly in every sentence (even with abbreviations like "IMO"), so sometimes a statement is left bare. When this happens, it is best not to latch on to it and complain that this particular thing has been stated "as fact". It is clearly someone's opinion when viewed in context.

For example:

No caveat here – you seem to be stating it will cause a lot of bugs as a fact. But of course you aren't – everyone understands that this is just your opinion. But the words used here are extreme. It will introduce a lot of bugs? The var in if var x is "nearly invisible"? This is clearly hyperbole (IMO of course) and I think the discussion would be better without extreme characterizations like this.

What (I think!) you mean is that you can envision a hypothetical scenario in which someone might think it's an inout binding and that this could lead to a bug occasionally. And I in turn think that this is not going to be a problem in practice. Nowhere else in the language does var do this, including the very similar if var x = x that already exists. And current use of that feature does not suggest this problem is causing "a lot of bugs" as far as I know.

This is a phenomenon we get a lot on these forums, which I call "performative misunderstanding", where a possible but frankly implausible misinterpretation is cited as a reason not to adopt a feature. I think it's a real problem, because it worsens good proposals, for example giving us waffly names for standard library functions. It can cause us to skew the design towards the very first use of a feature, not the subsequent 1,000+ uses, all the while not actually making that first use particularly better.

I wouldn't. I'm not suggesting we accept a proposal without considering how the future direction of borrows and moves might interact with it – though I do think you're overestimating the closeness of the design of type-system-level moves and also the likelihood it will affect this particular sugar.

What said in the text you quote is that to keep this discussion on track and hopefully converge on a proposal we could run is that we first discuss the language as it is today, and then once settled on a good potential addition, we think about its interaction with future syntax for borrows.

I also think there is a flip side to the "we've done without this for 7 years"... which is that we should be a bit sheepish about that. It's not great that this clear pain point has gone unaddressed so long, and putting it off another two years because it might not quite fit with a feature that hasn't really been designed or pitched yet is not OK. What we will probably need to do is make a judgement call about the likelihood of future directions clashing, maybe with some early sketches of what that feature might look like. But that is going to be a long and probably pretty hand-wavy discussion, and I think it's best had after we handle the already hard-enough task of settling on a proposal that works for the Swift of today.

21 Likes

I fear “performative misunderstanding” implies an intent to deceive. But you have very clearly highlighted a pervasive tendency in design discussions that I’ve seen here and elsewhere. I think it stems from a well-placed concern about “what if we get this wrong”? But this fear turns the process on its head. In my experience, the best designs emerge from a focus on self-consistency, conceptual thoroughness, and approachability. Not from future-proofing or avoiding interactions with other features.

In this case, we have a very clearly defined problem: the verbosity of optional unwrapping syntax discourages people from using descriptive variable names. @cal made a targeted concrete proposal: drop the duplication of the variable name, but retain the existing syntax. A bunch of concerns and alternatives have been raised, but as they have trended toward the more esoteric (e.g. new keywords) they have lost sight of the benefits of the original targeted proposal.

8 Likes

Yes this is a good summary of how I feel. My intention is not to imply bad faith, but rather to characterize a trap it is easy to fall into where excessive focus on downsides that aren't actually a concern in practice ends up worsening proposals.

8 Likes

I hope you’d also agree that not all forward-looking criticisms fall into this category. For example, I think it was fair for @Chris_Lattner3 to bring up if var and mutable reference bindings, if only to ensure they are contemplated. They’re “real enough” that the pitch can foresee potential conflicts or ambiguities. But that doesn’t need to dominate the discussion, dooming the original pitch and diverting attention to a new alternative that drops all the original pitch’s benefits in favor of avoiding any potential conflict.

3 Likes

To address the proposed alternative spelling:

One of the key behaviors of optional unwrapping is that it creates a new variable defined within the inner scope. When deciding what spelling to use for this shorthand, my top priority is making it as clear as possible that a new variable is being defined.

Since we use let / var for this elsewhere in the language, I think if let foo makes the variable scoping behavior reasonably clear and unambiguous. For if unwrap foo, I personally don't think it is "obvious" whether or not a new variable is defined for the inner scope. This can be learned / memorized of course, but I think this is the main downside of introducing a new keyword compared to leaning on existing keywords / concepts.

Another reason I think let / var is a good idea is that it makes the UX limitations a bit more intuitive. For example, it is not necessarily obvious that if unwrap foo.bar would be invalid. On the other hand, it is somewhat intuitive that if let foo.bar would be invalid (e.g. that let can only be followed by an identifier and not an expression) since this is the case elsewhere in the language.

Some other points that folks have mentioned that I agree with:

12 Likes

How about for x in y?
While I do believe the effort of changing the if let construct is not worth the rather minimal gains (if any!), there are precedents in the language when brevity was more important than spelling out/suggesting each step the compiler takes when translating the statement. And they work quite well, nobody's complaining why we don't use for let x in y.

1 Like

Yep, for foo in bar is one exception where we don't use let to introduce a new variable. Although imo this is because for in is a "term of art" present in a large number of languages and the creation of a new variable in the inner scope is clear by convention. Adding a let wouldn't really add much additional clarity.

We likely do need some keyword for optional unwrapping conditions, to avoid ambiguity with boolean conditions:

So in this case the let (or some other keyword) is pretty important:

1 Like

An alternative to a keyword is a sigil, as in:

if foo?, let bar = foo.bar {
  …
}

But I agree with your rationale for wanting let to appear in the unwrapping expression. It succinctly explains the shadowing behavior, which I don’t think any Swift programmer should be afraid of or averse to.

By the way, in the case of a multi-term defeatable condition, the shadowing caused by unwrapping would take effect for the rest of the expression, right? Otherwise I’d have to write (using my personally preferred syntax variant) if let foo?, let bar = foo?.bar { }.

My thoughts on this remain similar to what I had to say last May:

Since I haven't seen it otherwise mentioned in this thread, I'll also resurface what I think is important precedent in the language for having special language support for the "initialize a new binding that shadows an existing binding with the same name". We already allow this for closure capture lists:

So we aren't inventing an entirely new concept here.

As for if let x { vs. if let x? {: as noted above, I think if let already serves as a pretty strong indicator to Swift programmers that there's an optional unwrap happening, but the signal may not be as strong for, say, if var (or future if inout, if ref) unwrapping shorthand. So perhaps that's a case for requiring the trailing question mark, though also maybe those uses won't be common enough to justify 'muddying' the overwhelmingly common if let case. I think I have a weak preference for if let x? but could very likely be happy with either in the language.

8 Likes

As a Swift newbie coming from C++, that lack of let there actually confused me for a while wondering if using an existing local variable there reused it or shadowed it as I'm so used to for (int i = 0; i<n; i++) vs for (i = 0; i<n; i++)

3 Likes

As a Swift newbie coming from C++, that lack of let there actually confused me for a while

Fair enough, but my point is, people learn about it and carry on. I feel the brevity keeps on giving, while knowing there's an actual copy there it's a one time info which you don't necessary need to be reminded every time.

1 Like

I must be on another planet as almost everyone posting here, because if let foo { ... } to me looks dreadful.

I always read let a = b with "let" being a verb, as in Pythagoras declaring "let c be the triangle's hypotenuse" (although he'd be speaking Greek.. so bad example, but YKWIM). This reading doesn't really fit with "var", but I regard "var x" to be a term of art and read var x = y kind of as if it were let var x = y.

There are a few cases in Swift where one uses a loose "let foo":

  • let foo: Int: I read these as "let foo be an Int (defined below)" which is a reading that's helped by there always being a type after the colon

  • case .cat(let curiosity) in a switch or "if case" expression: these straight up give me cognitive dissonance and makes me wish the language did matching expressions differently. It's a red flag to me that these expression need to be mentally parsed at a more-conscious level. [1]

  • are there more I've forgotten about?

Contrary to people who've relayed their experiences teaching others, if let a = b made sense to me right off the bat: "if I'm able to let a equal optional value b". I feel the part about unwrapping is implicit compared to simply let a = b by being within an "if" expression, Sure, not obvious when you don't already know the idiom, but once you do I don't think there's any cognitive dissonance.


However if let foo looks like nothing to me. It adds yet another loose "let foo" to the language, one that would be used a heck of lot more than the cases noted above. To me it takes the cognitive dissonance of the lesser-used "case let" expressions and moves it to prime time.

Perhaps other coders in the thread read let a = b as immutable a = b, where "immutable" is an adjective (or whatever) instead of a verb, more akin to the plain reading of var x = y. Maybe this allows if let foo be less dissonant: "if I can make an unwrapped immutable variable out of optional value foo", or something. Or perhaps others aren't mentally mapping the code to English prose to the degree I do, and are just manipulating symbols.

The only two options I've seen in this thread that read acceptably to me are:

  • if unwrap foo: Plain. As. Frickin'. Day. [2]

  • if let foo = _: It probably depends on whether one thinks "_" means "something unsaid" or "don't care", and only making sense if you think the former not the latter. I don't think Swift uses "_" on the RHS in any other case, so free to indicate something new along the lines of "something unsaid", namely "the same symbol as on the LHS". Also as unclear to beginners as if let a = b is, surely more so in fact, but still comprehensible in my eyes after knowing the idiom.


  1. Don't get me started on how "if case pattern = thing" is backwards, after all I write "if thing < 5" rather than "if 5 > thing", why can't write something like "if thing matches pattern"? To say nothing of "if foo is in collection" rather than "if collection contains foo" ↩︎

  2. but not "if unwrap a = b", again reading "unwrap" as a verb applied to a, not as just an available symbol indicating something about unwrapping ↩︎

14 Likes

As long as there will be a construct for optional binding condition, there will be a learning curve for it, whatever syntax and semantics would be. We've learned if let x, and it happened we often use it to explicitly shadow the optional variable - as if let x = x. The proposal is pure evolutionary in that regard (with all its pitfalls), addressing what is recognized after the years of the use as verbose / noise. With the proposed syntax (and semantics untouched), I don't think the learning curve will be worsen. Sure it won't be improved but that is not the goal here. For those who learned the current syntax and semantics of the optional binding condition, they will likely use the proposed shorthand with the very same "correctness". I like how the proposed (subtle) change in the grammar hit the nail on the head - applies to all conditional control flow statements and preserve let and var. Personally I don't have a strong preference if I want it or not, or with if let foo? spelling. I can definitely live without it, but if it will be implemented, I'll use it.

unwrap keyword, borrowed values, and questioning the actual optional binding condition sound rather like revolutionary thoughts. They definitely shouldn't be dismissed but perhaps developed in another thread. If they will materialize, they won't challenge the proposal but will go even beyond optional binding conditions.

3 Likes

The unwrap notion inspires operators that I call peel and peel?.
if peel? x { is essentially the same as if unwrap x {. Let me know
what you think about peel.

The peel operator works roughly like with in Pascal. For any
struct/class x, peel x introduces a new scope that binds each member
name in x to a copy of the member's value in x.

The peel? operator removes a level of Optional<>ity. It is used in
if or guard conditions. For any optional x == .some(y), peel? x
is true; it introduces a new scope where x is bound to a copy of y.
For any optional x == nil, peel? x is false.

Under at least one circumstance it makes sense for peel and
peel? to compose: if peel peel? x {. I believe it makes sense
for there to be any number of ? after peel: peel??? for
Optional<Optional<Optional<_>>>.

Examples:

struct Record {
        var a: Int
        var b: Int?
}

struct CompoundRecord {
        var c: Record
        var d: Int
}

let compound: CompoundRecord
let rec: Record
let optRec: Record?
let y: Int
let z: Int?

if peel rec {     // error: `peel rec` always succeeds
}

peel rec {
        // In this scope, a and b are copies of rec.a and rec.b, respectively.
}

peel y {        // error: `y` has no members
}

peel? y {       // error: `y` is not Optional<>
}

peel z {        // error: `z` has no members
}

peel? z {       // error: `peel? z` can fail
}

peel optRec {        // error: `optRec` has no members (it is Optional<>)
}

if peel? z {
        // In this scope, z in the outer scope is shadowed by a non-optional
        // copy.
}

if peel? optRec {
        // In this scope, optRec in the outer scope is shadowed by a
	// non-optional copy.
}

if peel? optRec, peel optRec {
        // In this scope, optRec in the outer scope is shadowed by a
	// non-optional copy.
        // 
        // Also in this scope, a and b are copies of optRec.a and optRec.b.
}

if peel? rec.b {
        // In this scope, b is a copy of rec.b!.
}

if peel? optRec?.b {
        // In this scope, b is a copy of optRec!.b!.
}

if peel peel? optRec {
        // `optRec` is not shadowed in the inner scope.
        // 
        // Also in this scope, a and b are copies of optRec!.a and optRec!.b.
}

if peel compound.c {
        // In this scope, a and b are copies of compound.c.a and compound.c.b.
}

Dave

1 Like

I think this would be an ergonomic non-starter as a replacement for if let x = x?.y?.z, as the current construct automatically removes every level of optionality.

I do like how the two uses of peel have a certain underlying similarity, as Optional<T> is a kind of wrapper over T, which means that peel? would be peeling away the optional and leaving just the T value. I fear, though, that this would be too subtle for people to understand.