[Pitch] Non-nil assignment and shorthand nil checks

Hi everyone! We oftentimes need to assign some value to a variable only if it's not nil, this leads to us having a code structured like this:

if let foo {
    bar = foo
}

This is already pretty compact but I'm here to suggest an even shorter version to this:

bar = foo?

Similar to how Swift already has bar? = foo to assign foo if bar != nil, code from my suggestion will assign foo to bar only if foo != nil

This can be expanded to another use case. Pretty often I find myself having a switch-case statement that goes over an optional value, I'd suggest we use the same operator here to write this:

switch foo? {
...
}

Instead of this:

if let foo {
    switch foo {
    ...
    }
}

Or this:

switch foo {
...
case .none: break
}

This also would imply any optional value can be used there, so would also be a valid code:

foo = bar()? // bar() -> Optional<...>
foo = (try? bar())? // bar() throws -> ...
foo = (await bar())? // bar() async -> ...

However I'm not particularly fond of how the last 2 lines look like but, well, no one forces us to right such code

Thanks for having a look, I'd be glad to discuss it in detail

Maybe this?

infix operator =? : AssignmentPrecedence

func =? <T> (lhs: inout T, rhs: T?) {
    if let rhs {
        lhs = rhs
    }
}

bar =? foo

Otherwise it's too confusing for my brain as it considers there's an assignment going on all the way up until it sees the trailing question mark.

Whether this should be in the standard library is another question.

The good point about your syntax (even if it's unprecedented) is that we could potentially use it for other assignment operators:

bar += foo?

without defining several new "optional" versions:

bar +=? foo

Although not for arbitrary operators:

bar < foo? // 🤔

unless we further redefine (complicate) the language to treat that returning an optional result in this case:

let result = bar < foo? ?? false

In both of these alternatives: will lhs be evaluated when rhs is nil? autoclosure doesn't work with inout argument, so the answer is yes for the operator version and not so obvious for your version. The original example doesn't evaluate bar if foo is nil

if let foo {
    bar = foo
}

BTW, I see no serious issue in this fragment:

switch foo {
...
case .none: break
}

hi @tera, thanks for reply. it didn't come to my mind that one could use it with e.g. += which is also a great thing, love it! My idea was that my solution wouldn't evaluate bar unless foo has value.

indeed, your =? operator solves part of the issue, thank you!
there's nothing inherently wrong about case .none: break but I found myself often not caring about nil case and it would be cleaner iterating just over the cases the non-optional value provides

let result = bar < foo? ?? false shouldn't be possible imo. foo? in this case isn't checked or assigned but actually used so this particular example indeed makes things more complicated and I wouldn't want such a thing be in Swift. at least as it looks now. this is the reason I didn't suggest to use something like this:

foo = bar(baz?)

while it can work as "don't evaluate the function and assign the result as implication" this makes the feature much more confusing

1 Like

This is not a real assignment operator because it can't be used to initialize a variable. The only possible assignment operator is "=" https://github.com/apple/swift/blob/00729ad95843be779d0338a5530b90432d4df03e/stdlib/public/core/Policy.swift#L518
You can check with

var bar: Int
bar =? foo // won't compile

Another issue with this is that nobody knows direction of action it does. Everybody know that bar = foo is "assign foo to bar", but bar =? foo could mean anything.

This feels wrong because it reads not continuously (neither left-to-right nor right-to-left). The optional chaining operator should be next to the operation that will be executed after unwrapping. To make it consistent with the language you'll need left-to-right assignment operator first (foo?→bar).

1 Like

@dmt fair point, thank you!

And it should not... Neither the

var bar: Int = foo?

or any other variant. What would bar be if it is not initialised?!

In case of "assignment when the source operand isn't nil" it really doesn't matter, but if you consider a custom non-conditional assignment operator you want it to be able to initialize variables (not only re-assign). Like in example I mentioned above - left-to-right assignment, but without "optional" part:

var bar: Int
foo → bar // it should compile, but it doesn't

Which as you just mentioned we can't have in the current language :-)

Why would I want to use:

var bar = 0
var foo: Int → bar

when I can use a mere "="?


Syntax optimising the statement from the headline post makes certain sense to me:

if let foo {
    bar = foo
}

and obviously here it is an assignment, not an initialisation.

I also do like:

and the venues it opens. Maybe we can warn users to pay more attention to this line? Bike shedding:

optionally foo = bar(baz?)

I think this form is the best, and it's the most informative for programmers, because it explicitly shows that bar assignment is done only through a certain code path, so if bar wasn't already initialized before the if block, it cannot actually be considered initialized after it, and cannot be used.

That is, this code would not (and should not) compile:

let bar: Bar
if let foo {
  bar = foo
}
_ = bar // error: used before initialization

So, a specific operator is impossible to use without adding extra functionality related to code path analysis to operators.

The compiler could understand that something like

let bar: Bar
bar = foo?

doesn't guarantee initialization, but this doesn't read right to me: it looks like bar is initialized with nil if foo is nil.

I think the symmetry between bar? = foo and bar = foo? is misleading, because bar? = foo implies that bar has been already initialized, either to some value or to nil, explicitly or implicitly (in fact, writing var bar: Bar? will implicitly initialize bar to nil). To see better the trick, we can see that this code doesn't compile, but not for the reason one might think:

let foo: Bar?

let baz = Bar()

foo? = baz

the error is not that foo is let and cannot be assigned: the error is Constant 'foo' used before being initialized because let foo: Bar? doesn't implicitly initialize foo to nil.

Thus, bar = foo? should only work if bar has been already initialized, but using the regular assignment operator makes it, to me, misleading.

In some cases, code that

  • uses syntactic sugar built on top optionals, and
  • produces side effects (like assignment)

can become unnecessarily harder to understand, because makes the branching paths unclear.

For example, something like

foo?.bar.forEach { ... } // I'd prefer a regular for-in here, but this can be useful for other reasons

while clean and concise, appears to me less clear than

if let foo {
  foo.bar.forEach { ... }
}

The situation is similar for assignment, with the added bonus that delayed assignment uses code path analysis to make sure that a variable or constant has been assigned when used.

You woudn't. But if you have ?→ it makes sense to introduce → just for consistency.

To be fair, neither will any of the other AssignmentPrecedence operators like +=.

2 Likes

This pitch is good but too constrained. It needs to apply to anything that is a throwing property wrapper. Neither Optional nor Result have been converted to be so, but they need to be, as that's what they represent.

As it is, what @tera said is necessary:

public extension ThrowingPropertyWrapper {
  /// Assign only non-throwing values.
  static func =? (self0: inout Self, self1: Self) {
    if .success(catching: try self1.wrappedValue) {
      self0 = self1
    }
  }

  /// Assign only non-throwing values.
  static func =? (wrappedValue: inout WrappedValue, self: Self) {
    try? wrappedValue = self.wrappedValue
  }
}

But @ramzesenok's proposed syntax is what actually should be used. ? should consistently mean "or do nothin' " when it comes afterwards. E.g.

var result = Result { "🪕" }
var success = "🎻"
result =? .init { success }
success =? result

should be

@Result var result = "🪕"
var success = "🎻"
result = .init { success }?
success = result?

I can understand that reading, but that's already what = does without ? appended. ? should mean something different.

As a workaround, bar = foo ?? bar will have the same result as if let foo { bar = foo }, just possibly less efficiently. If bar is known to be stored with no accessors I’d expect it to optimize to the same thing (though I haven’t checked).

1 Like

bar must be evaluated twice here, no? e.g. if bar has willSet/didSet those will be called.
If we are looking for a "syntax optimised" version equivalent to if let foo { bar = foo } bar should not be evaluated when foo is nil.

Yes, that’s the “but possibly less efficiently”.

1 Like

Well, even if bar is a computed property, it'll be gotten exactly once and set exactly once in that expression, which is about as efficient as a computed property can get. If it's a multi-part lvalue I guess you could have some reprojection overhead, though.

Yes, but it ought to be zero times if foo is nil.

It's not so much the efficiency (which is of course important), but correctness I worry about, when I change the app from:

    if let foo {
        bar[baz()] = foo
    }

to a shorter:

    bar[baz()] = foo? 
    or
    bar[baz()] =? foo

where:

    var bar: [Int] = ... {
        willSet {
            // some side effect 1
        }
        didSet {
            // some side effect 2
        }
    }

    func baz() -> Int {
        // some side effect 3
    }

that would be an observable change of behaviour in the app after the change.

@tera exactly! this is the reason why I wanted it not to evaluate left side of assignment. lazy properties would also suffer from evaluation

This is the best I can think of that doesn't require compiler change:

protocol AssignTo {
    func assign(to x: inout Self)
}

extension AssignTo {
    func assign(to x: inout Self) {
        x = self
    }
}

extension Int: AssignTo {}

func test() {
    var x: Int?
    
    var y = 42 {
        didSet {
            print("didSet")
        }
    }
    
    x?.assign(to: &y)
}

test()

Maybe with macros it could be done better?


I remember wanting something similar with a shorter equivalent of:

if x != y {
    x = y
}

the equivalent that (as the original) won't trigger willSet / didSet unnecessarily.

1 Like