Let's fix `if let` syntax

FWIW, when it’s just a simple property access, in our codebase it’s mostly if let thing = other.thing and not if let otherThing = other.thing. I think the reason is that often there’s only a single other you’re doing something with, so the context is clear enough. The unwrappings that get more descriptive names are the ones where there is a longer optional chain or function call involved. But even then, sometimes it’s just if let thing = other.third?.thing.

1 Like

I don't know how universal that is and I don't like that mentioning one name (e.g. other.thing) creates a variable with a completely different name (e.g. thing). I wouldn't like var other.thing to mean var thing = other.thing either. You're at the point of diminishing returns when you're just avoiding repeating a part of the name.

if let x has an easily teachable explanation as a shorthand syntax for if let x = x, similar to the explanation of the various shorthands for closures, and I don't personally have an desire to make it more complicated than that.

6 Likes

Because using it here would be overloading it in a new and different way. This erodes and dilutes the existing meaning of ? in patterns by addition a different meaning in a very similar syntactic construct. I think such a move would make the language more confusing and would make patterns more difficult to explain. Since there is no obvious reason we need to do that, I recommend we pick a different form. This is really the crux of why the discussion went no where in the past.

I'm not arguing for unwrap specifically, I'm arguing that we use a new explicit form to represent this new concept. I'm not sure I agree with your argument against unwrap though: it isn't onerous to require a short word over a sigil. The whole point of the sugar argument is the DRY thing, which is still accomplished.

-Chris

3 Likes

if let x { is better than if let x? {, but omits the action from what is happening here. This is obvious if you memorize it, but doesn't help new programmers.

The problem with it is that it begs the question about "can I do if var x {. What is your opinion?

-Chris

1 Like

if let x = x already omits the action of unwrapping so I'm not personally concerned about also omitting the explicit copy here. It seems very teachable, in the same way that shorthand closure syntax is currently taught by showing equivalent forms and sequentially omitting parts. I actually didn't know that you could already write if var x = x to unwrap, I've only ever used the let form, but given that is allowed then I don't see the harm in allowing if var x also.

10 Likes

I broadly agree with others in the thread saying that if let x { ... } feels like the most 'natural' spelling for this shorthand. I think the let or var introducer is important for communicating that there's a new binding occurring, compared to e.g., the if x? { ... } alternative (and yes, I think var should be supported).

As for "omit[ting] the action," we already have another place where we allow a variable name on its own to be expanded to an initialization of a new variable with the same name:

let closure = { [x] in // implicitly: 'let x = x'
    ...
}

This works even if x refers to e.g., self.x in the outer scope.

In this case we don't even force (or permit) the user to specify let to indicate that there's a new binding occurring. However, I've found the lack of an introducer to be a hinderance for users trying to understand what's really going on in a capture list, and IMO it would be even worse of conditional binding since there's really two things going on at once—checking for nil, and binding a new name.

Overall, if let x { ... } feels like a good (new) balance of brevity and clarity. It maintains the existing 'if let' phraseology that I expect most Swift programmers have come to instinctively understand as "bind if non-optional," and the additional behavior is (IMO) easily explainable. It feels so natural to me that I semi-frequently find myself writing it and getting a syntax error before remembering that it's not already supported!

21 Likes

I missed that on the first reading. I deleted my comment.

if let x = x { } already has some form of privileged syntax: x.map { }.

Optional.map is even defined in the documentation as an alternative to if let, without even referencing the closure’s return value at all:

Evaluates the given closure when this Optional instance is not nil , passing the unwrapped value as a parameter.

That doesn't help with guard let, though.

3 Likes

A bit off-topic, but I was just trying out guard let x = x and found that it compiles in some scenarios but not others:

// At global scope

// Compiles

let x: Int? = 1

guard let x = x else {
  fatalError()
}
// Not at global scope

// Error: definition conflicts with previous value

do {
  let x: Int? = 1
  
  guard let x = x else {
    fatalError()
  }
}
// In a different scope from the original x

// Compiles

do {
  let x: Int? = 1
  
  do {
    guard let x = x else {
      fatalError()
    }
  }
}

(All tested in a playground with Xcode 11.3.1)

• • •

Edit: global scope allows weird things…

// This compiles and runs just fine:

let x: String? = "abc"

print(type(of: x))    // Optional<String>

guard let x = x?.count else {
  fatalError()
}

print(type(of: x))    // Int

But this does not compile:

let x: String? = "abc"
let x = x!.count
// Error: Variable used within its own initial value

And neither does this:

let x: String? = "abc"
let x = 3
// Error: Invalid redeclaration of 'x'

They all compile successfully in Xcode 12.5

got //Error: Circular reference,
but it makes sense.

Because I regularly use guard clauses to exit early, using var instead of let about 8% of the time, I think any change should handle var and let in both guard and if statements. It would be unfortunate if any syntactic sugar only handled one of these four cases.

7 Likes

Are you saying that this compiles in 12.5?

let x: String? = "abc"
let x = 3

What about this?

do {
  let x: String? = "abc"
  let x = 3
}

And this?

do {
  let x: String? = "abc"
  guard let x = x?.count else { fatalError() }
}

And, for completeness, this?

do {
  let x: String? = "abc"
  guard let x = Optional(3) else { fatalError() }
}

last two examples failed, but they should be failed.

I tested it in do{} and func block level, all compile succeed.

What is your " Not at global scope"?

Hmm... if Optional.map support else closure action, then x.map {} else:{...} should be the option.

You can use ?? for the else case. x.map { doThing(x: $0) } ?? doOtherThing().

1 Like

I'm all in favor of people using map when it's appropriate, but it is not the same. map is for mapping – an expression transforming the wrapped value inside an optional – not general purpose code using the wrapped value.

It also doesn't solve the issue at hand. Even if you were willing to use it (IMO inappropriately) as a general-purpose alternative to if let, then x.map { } is equivalent to if let $0 = x { }, which is exactly the kind of shorthand variable @chockenberry is saying is bad but the status quo encourages. Alternatively, if you think you should name the closure arguments, then you would need x.map { x in } which lands us back at square one.

(Also, by "privileged" I am speaking about adding custom handling into the language – in this sense Optional.map isn't really privileged because while it exists in the standard library, the same function can be written outside the std lib just as easily)

17 Likes

Overall, I'm sympathetic to the impulse to revisit this topic (if skeptical that this iteration will move the needle). But this formulation of @chockenberry's motivating problem, taken together with @jrose's examples of what would need to be excluded at least as a first pass, seems to slice the problem exceedingly fine:

  1. The variable x needs to be meaningfully named, such that it's important to write the name out n times inside the 'if' statement for clarity to the reader.
  2. It needs to be cumbersome enough, however, that writing it out n + 1 times (due to the unwrapping assignment) is once too many.
  3. The name, though cumbersome, needs to be so aptly named that even shortening it for ease of writing n + 1 times would lead to suboptimal clarity.
  4. But not so perfectly named that a refactoring wouldn't possibly rename it, such that the unwrapped name would then be inaccurate.
  5. It can't be a nested name x.y, or some other scenario mentioned by @jrose where the name is likely to be under the control of another party.
3 Likes

FWIW, I don't think that the items that @jrose called out need to be excluded in the first iteration of this feature. I think it's also a perfectly reasonable position (in addition to "these corner cases are excluded") to say "the if let x { ... } form is sufficiently indicative of rebinding/shadowing behavior, and so behaves exactly like if let x = x { ... } in all cases." It doesn't solve the issue with nested names (aside from self), but for me it would address the vast majority of situations where I find myself reaching for this feature.

IMO, the "cumbersome" aspect of this pattern isn't the n to n + 1 step for the entire if statement, it's the 1 to 2 step in the binding itself. For a verbose variable name this results in a long line that is needlessly repetitive and more difficult to quickly scan (on top of the autocomplete and refactoring issues that @chockenberry has called out).

2 Likes

Right, so we're thinking about the situation where the variable must be too verbose for the 'if' expression to read well, but not so much that it affects the readability of the other n uses inside the braces such that it isn't better off renamed upon unwrapping.