A More Swifty Way to Check an Optional for a Nil Value

Regarding my example:

I could imagine something like it, being used under
some circumstances, but I should maybe have come up
with a better example. I'm just trying to demonstrate
a principle here.

The infix operator seems interesting. Could you show
how it would be used in the example I provided?

Thanks in advance.

There is already a very concise way to achieve exactly this:
someInt? = 10

7 Likes

Thanks Tino, but I only want to set someInt to 10 if someInt
already has a value. If someInt was initially set to nil, then
someInt will remain nil. Maybe I should have explained this.

I think I have two objectives:

  1. To see if it is possible to never have to use an exclamation
    mark to unwrap an optional.

  2. To use the value of an optional if it has been set, and also
    change its value, without having to introduce a second,
    different variable name.

It seems to me that it is not possible to have both 1) and 2).

This satisfies 1), but not 2):

var someInt: Int? = 5

if let newInt = someInt { Swift.print("\(newInt)"); someInt = nil }

This satisfies 2), but not 1):

var someInt: Int? = 5

if someInt != nil { Swift.print("\(someInt!)"); someInt = nil }

If you're not willing to introduce a new variable name at all, then no, it's not currently possible to achieve both goals at the same time.

If you're willing to split the difference, though, and use an anonymous closure parameter name, then as noted a ways up-thread, the operation you're looking for already exists as flatMap(_:):

var someInt: Int? = 5
someInt = someInt.flatMap { $0 + 10 }
print(someInt) // 15

someInt = someInt.flatMap { _ in nil }
print(someInt) // nil

someInt = someInt.flatMap { $0 + 10 }
print(someInt) // nil

If this is too verbose for your taste, you can also avoid the repetition of someInt with a small wrapper:

extension Optional {
    mutating func flatMapInPlace(_ map: (Wrapped) throws -> Wrapped?) rethrows {
        // Note that there are slightly more efficient ways to write this,
        // but this is the simplest.
        self = try self.flatMap(map)
    }
}

var someInt: Int? = 5
someInt.flatMapInPlace { $0 + 10 }
print(someInt) // 15

someInt.flatMapInPlace { _ in nil }
print(someInt) // nil

someInt.flatMapInPlace { $0 + 10 }
print(someInt) // nil

So, you want something more like:

extension Optional {
    @inlinable
    public mutating func mapInPlace(_ update: (inout Wrapped) -> ()) {
        if var value = self {
            self = nil
            update(&value)
            self = value
        }
    }
}

do {
    var x: Int? = 42
    x.mapInPlace { $0 *= 69_105 }
    print(x ?? "nil")
}

I just wish we had a better name for this method. For collections it can be updateEach, but that's not a great name for the optional version.

I see, very interesting tricks that I hadn't counted on.

In any case, it's very good to know that it could be done this
way also – so, thank you both for taking your time with this!

But that's exactly what Tino's fragment is doing...

var someInt: Int? = nil
someInt? = 10
print(someInt) // nil
var someInt: Int? = 1
someInt? = 10
print(someInt) // Optional(10)

Although I never encountered this syntax before, it's quite an obscure feature.

I wonder though what exactly is the problem here? As written it is totally fine with me (when it is in other people code, see 1). Introducing prettifiers like "someInt.isNil" or "someInt.isNotNil" or "Bool(someInt)" doesn't buy you much, and if it does – you can create such an extension and use it throughout your code base (1).

(1) I do something along these lines in my code base having custom "a <> b" and "a.not" operators (I just don't like explanation marks and also prefer negation to be postfix)).

1 Like

OK, I see now that it works! I had never seen this before.

There is no big problem with using != nil, but diamonds are
neater, in my opinion.

Regarding my two objectives:

I think I have a solution for people with exclamation mark phobia.

If I cheat a little, and use my precious diamonds, both my goals
can be achieved:

var someInt: Int? = 5

if <>someInt {
    if let someInt { Swift.print("\(someInt + 1)") } // 6
    someInt = nil
}

Swift.print(someInt) // nil

It's akin to the famous egg of Christopher Columbus.

I've seen it and forgotten that it was the reason I wrote that operator above, to achieve consistency with the other side of the assignment operators.

var none: Int? = nil
none?=1 // nil

var zero: Int? = 0
zero=?none // 0

You just can't use enclosing spaces (none ?= 1) because ?= is not an operator.


Looking at it this time, I thought at first this was a bug with optional chaining in that maybe an operator that returned Void was required…

someInt? %= 2 // compiles
someInt?.quotientAndRemainder(dividingBy: 2) // compiles
someInt? % 2 // Value of optional type 'Int?' must be unwrapped to a value of type 'Int'

…but no. There's more specific magic at work. You can only recreate the behavior with what's noted in the next post. Return type doesn't matter.

infix operator •: AssignmentPrecedence

extension Int {
  static func • (_: inout Self, _: Self) -> Self {
    123
  }
}

var someInt: Int? = 0
someInt? • .max // 123

You can use optional chaining with an operator if its precedence group has assignment set to true. The following code does compile:

precedencegroup MyPrecedence {
    assignment: true
}

infix operator •: MyPrecedence

extension Int {
    static func • (_: inout Self, _: Self) {}
}

var someInt = Optional(0)
someInt? • 2
3 Likes

I’ve been thinking a lot about custom operators. When I code, I follow a number of conventions (as do we all) with the goal of producing unsurprising code, I.e. code that does what it appears to do. And, I’m in the crowd that thinks + is a great glyph for concatenation, and strX - strY looks like nonsense.

If a new coder comes to look at your code, new operators really need to be super obvious. This is why I made a custom operator to convert CGFloats to Double, and then removed it and extended Double with a function to give me a cgfloat instead. The operator failed to convey what it did.

One of the worst abuses of obviousness is the use of < and >. Like most everyone, I learned that < means less than, and > is greater than. ‘->’ is a little weird, but the arrow makes sense pretty quickly. cout << “foo” is gross if you see << as meaning left shift.

HTML makes me cross.

Foobar<T> makes me grind my teeth. I thank what deities might approach that Swift avoids the absurd levels of use of that construction that C++ has.

To me the Swifty way to check for a nil optional is

if foo != nil {}

It’s what I’ve been doing since Optionals were introduced. Sure it’s a tiny bit more typing. But it’s blindingly obvious what it does.

4 Likes

TOL: If this does nothing (as it is now):

var someInt: Int? = nil
someInt? = 10

then perhaps this should do nothing as well:

var x: Int?
let y: Int? = x? + 1

and this:

var x: Int?
func foo(_ x: Int) {}
foo(x?)

It would be awkward still, but at least consistent.

1 Like

Ideally it could be like this:

var someInt: Int? = 5

if let someInt { pre.someInt = someInt + 5 }

Swift.print(someInt) // 10

That is, pre is somewhat like self, but it
refers to the original optional variable.

Off-topic, but this is making me wish Swift had a built-in null assignment operator (??= in some languages). Far more useful than this obscure non-null assignment, which as you mentioned earlier you're having a hard time imagining a use case for. Is there documentation on this anywhere?

1 Like

Yeah, I did see that. That was prior to its popularisation in other languages (including C#, Javascript, and PHP), so I'm hopeful it may be reconsidered one day :)

I was wondering if there was documentation on the existing someInt? = 10 feature though?

I second that :slight_smile:

3 Likes

also, since optionals are enums under the hood you could also do something like this:

if case let .some(value) = foo {
   // do something with value
}

// or if you dont care about the value

if case .some = foo {
    // do something
}
1 Like

personally i don't think Swift .map or .flatMap act as kotlin "let". Kotlin "let" doesn't iterate over an array for what i know. If you are looking for something more like kotlin let maybe that fit:

extension Optional {
  @discardableResult
  func `let`<T>(_ unwrapped: (Wrapped) -> T?) -> T? {
    if let self {
      unwrapped(self)
    } else {
      nil
    }
  }
}