Let's fix `if let` syntax

@Marc_Palmer (I forgot to click the reply button :innocent:)

Thank you for clarifying. I agree with the treat of x.y = * and the problem of renaming.
I'm a bit concerned about cases where unluckly it didn't cause compile error (when we have another variable whose name is equal to new name), but it would be trivial.

Currently, while you write if let x = x {} in Xcode, for the first x, autocomplete does not work as written in the initial post.

In the same way, in if let x = * {}, autocomplete for the first x would not work still. I don't think it is a serious problem, but I concern for some 'lazy' people it would be still annoying. I agree that = * can reduce needs for autocomplete on the right side.


How about labeled function? If range(from: *, to: *) means range(from: from, to: to), I think it potentially discourages people from defining function that uses preposition as labels like range(from num1: Int, to num2: Int) -> Range<Int>, because no one wants to have variable named from or to even as local variables. I don't know how much it matters, though.

My pushback to introducing a new sigil to represent "redeclare this variable" isn't affected by the choice of sigil so * being a placeholder is not important.

Such syntax being out of keeping with the general direction of Swift aside, I don't think "redeclare a shadow copy" is a good pattern to encourage, so yes, I think keeping a focused solution of if let x { or similar would be a better solution than attempts to generalize.

For example, another common case of "redeclaring" a variable of the same name is something like this:

  func addSamples(_ newSamples: [Sample]) {
    var newSamples = newSamples
    // perform mutation on samples in various ways
    self.samples += newSamples
  }

Generally speaking, you probably want to name the mutable newSamples to something different – something representing the updates you're applying before assigning them. Actual shadowing could cause significant confusion between these two now very different variables. But var newValues = * shorthand would encourage this situation.

if let x { does not have this problem. x is fundamentally the same x, except unwrapped. The name sharing is perfectly legit.

Now this does lead to the question of whether if var x { should be allowed (after all, you can today write if var x = x). My feeling is maybe not? There is already significant potential for confusion with if var x = x because you are not mutating the outer x, but at a glance you might think you are. This is similar to the confusing and since-removed f(var x: Int) syntax as others have noted. Then again, some day hopefully we will get the ability to do in-place mutation i.e. if var &x { x += 1 /* mutates value inside outer x */ } at which point the same-name would once again be appropriate.

Your other compelling use case is in initializers. I definitely agree initializers are an ergonomic pain point. I suspect that this needs different solutions though – some kind of targeted member initialization syntax that lets users more flexibly declare which properties a user should be able to supply to an init and automatically applies them. It's possible an idea like = * could end up being the right solution here, though I am skeptical. But initializer ergonomics is a big topic that deserves its own thread instead of a tangent discussion here. And also one that is unlikely to be solved in the very near future. It would be a shame to hold up what seems like a much more of a targeted and achievable solution for if let specifically.

9 Likes

Will if let work for properties?

if let list.first {

1 Like

I think the version I want most often is “guard var”, as in:

let j = try firstIndex{ try !predicate($0) }

guard var i = j else { ... }

That works, but introduces an extraneous identifier j.

It’s possible to eliminate j by writing this:

guard var i = try firstIndex(where: {
  try !predicate($0)
}) else {
  ...
}

But that just seems…unappealing.

I’d rather be able to write something like “guard unwrap i”:

var i = try firstIndex{ try !predicate($0) }

guard unwrap i else { ... }

Just one data point from someone who started pick up swift recently. The current syntax is a bit jarring to the eyes as it first looks like a self assignment. Not sure what would be the best syntax, but definitely agree it would be nice to improve this (the simple if let x { seems ok). So +1 to the pitch sentiment.

2 Likes

A better link is Introducing `Unwrappable`, a biased unwrapping protocol

2 Likes

There seem to be two concerns that prompted this discussion:

  • no autocomplete for the LHS of this pattern, which makes long variable names annoying
  • refactoring does not recognize the shadowed name as being the same as the original

Both of these are tool problems, specific to Xcode (re: item 1, other editors do basic string-based completion just fine). And I think it's important to note that neither of them is automatically fixed by new syntax, because the tool has to be updated anyways to support whatever the new syntax is.

8 Likes

I personally don't see a reason to implement a new syntax.
Using Kotlin a lot, they have 5 scope functions, let is one of them:

extension String /* Any */ {
    func `let`<R>(block: (String) -> R) -> R {
        return block(self)
    }
}

let s1: () = "s".let { s in
    print(s) // s is String
}
let s2: ()? = (nil as String?)?.let { s in
    print(s) // s is String
}

Swift "just" needs to implement these functions too.

Kotlin has an also function too, which execute the block with the variable, but returns the variable.

extension String /* Any */ {
    func also(block: (String) -> Void) -> String {
        block(self)
        return self
    }
}
let s3: String = "asdf".also { s in
    print(s)
}

For the other scope functions (apply, with, run), Swift needs [Pitch] Functional - Clearer Design for Functions with Receivers

4 Likes

Gratuitous bike-shedding of keywords, what about:

if some fooViewController { ... }

It reads slightly better to me (certainly no worse) than have, and is consistent with the underlying enum.

3 Likes

I think, if we would go in the direction of adding a new keyword to achieve this, I wonder if it wouldn't be more useful to have a higher level feature that allows to implement such a keyword in the language itself (similar to property wrappers).

I'm thinking of something like this:

if $unwrap myOptional {
}


extension Optional {
    func $unwrap() -> Wrapped? {
        return self
    }
}

this code would be equivalent to

if let myOptional = myOptional.unwrap() {
}


extension Optional {
    func unwrap() -> Wrapped? {
        return self
    }
}

The neat thing would be you could use it for other use cases as well. For example verification:


if $verified userInput {
}


extension UserInput {
    func $verified() -> VerifiedUserInput? {
      // If `self` (UserInput) is valid we return .some(VerifiedUserInput) and
      // the if body is executed and the variable is reassigned to `VerifiedUserInput `.
      // Otherwise we return `nil` and the `else` branch is executed.
    }
}

This could then also be extended to support parameters:

if $verified(method: .foo) userInput {
}


extension UserInput {
    func $verified(method: VerificationMethod) -> VerifiedUserInput? {
        return ...
    }
}

map is supposed to just unwrap the value and transform it, like in an array map. Just like we have a forEach to perform operations on each element in an array (please don't use map for this!) we should have a corresponding method for optionals:

foo.ifPresent { print("Foo is not nil, it's \($0)") }

2 Likes

I think the argument is that, since you will need to repeat the name, you could be tempted to make it shorter than what would otherwise be optimal.

The stated problem is that there is no support for optional binding in the autocomplete feature nor the refactoring feature of Swift’s tooling. As expressed previously by @QuinceyMorris and @Terje (and perhaps others… long thread), I think the immediate—if not long-term—solution should be tooling changes rather than language changes.

3 Likes

I agree with improving tooling support. Specifically with suggesting Optional variables first after an if let ... = et similia.

However, I don't personally think a refactoring involving shadowed variables should exist. In the event that the user explicitly wanted to differentiate the name of the variable from the name of the shadowed one, how would the refactoring proceed if the editor automatically and indistinguishably renames everything?
That would create a situation in which, if you happen to have named a variable with the same name of the variable you are shadowing, then you can't go back differentiating them.

Actually, I would argue there are two important sentences that go hand in hand::

I agree that this is a common pattern, and it would allow you to “write less code”, but that isn’t the goal of Swift.

and

Reducing syntax isn’t itself a goal, particularly if the result could/would be confusing for someone who has to read and maintain your code later.

Quite frequently I have found “syntactic sugar” in other languages to be maintenance poison.

Sure, your code gets shorter but now no one can maintain it. PERL is my goto punching bag for this. When our PERL guru left for two weeks and it fell on me to fix a bug, I spent two nights crash coursing on PERL. I fixed the bug, but to this day I can’t say I know PERL.

Terse code frequently becomes code that is tossed by those who can no longer maintain it.

Coding for maintainability should be the nirvana of all programmers.

6 Likes

since if let x = x is essentially unwrapping,
why not use the char that's already associated with unwrapping(!) but associate it with the let itself. no new keyword or operators :
if let! x{....}

I like this idea.

Similar to as! and as?, having if let! and if let? feels very natural to me:

if let? x {
  x.foo() // x has type “Bar”, equivalent to “if let x = x”
}

if let! x {
  x.foo() // x has type “Bar!”, crashes if nil
}

Arguably the latter is less useful but it narrows down semantics to “bind if not nil” and “bind anyway, I know what I’m doing”.

EDIT: Added examples of use.

2 Likes

! is used specifically for forced unwrapping, meaning “crash if this is nil”. Using if let to unwrap is the exact opposite.

7 Likes

Yeah, ! means that it can crash at runtime. Unfortunately there is no static version, where the compiler proves that it NEVER crashes, I will call it !! here:

if a != nil {
     print(a!!.someThing) // cannot crash at runtime
}

Writing only

print(a!!.someThing)

will give a compile error a cannot be proven non-nil!

That's an easy question, you already have the ability to opt-in to some types of renaming, for example variable names used in comments (as far as I remember).

Terms of Service

Privacy Policy

Cookie Policy