Implicit optional unwrapping in simple ternary conditional expressions, i.e. a != nil ? [a] : []

I often end up writing code like:

Example A:

let item: Type? = ...

let array: [Type]
if let item = item {
    array = [item]
} else {
    array = []
}
// Use array

and, maybe even more common:

Example B:

func f(_ Type) -> ReturnType {
    ...
}

let item: Type? = ...
let result: ResultType
if let item = item {
    result = f(item)
} else {
    result = .none // nil, [], [:], or some other representation of empty/initial state.
}
// Use result

Which are both tedious ways of writing something that is quite simple and easy to understand.
This can, in my very personal opinion, break the flow when skimming through code - I need to stop and think for a while; what's really going on here?

I would suggest that we allow simple ternary conditionals that are checking for nil of a single variable to implicitly unwrap that variable in the "then"-part. So that we could write:

Example A':

let item: Type? = ...

let array: [Type] = item != nil ? [item] : []

// Use array

Example B':

func f(_ Type) -> ReturnType {
    ...
}

let item: Type? = ...
let result: ResultType = item != nil ? f(item) : .none
// Use result

I'm not really sure about how to deal with the opposite case, i.e. item == nil ? .none : f(item). That may be a bit too weird to allow.

You can use already existing map. Note that it's a method defined on Optional, not on Type, so there's no question mark before the dot.
Example A:

let item: Type? = ...

let array: [Type] = item.map { [$0] } ?? []

Example B:

func f(_ Type) -> ReturnType {
    ...
}

let item: Type? = ...
let result: ReturnType? = item.map(f)
11 Likes

Another one for Example A:

let array = [item].flatMap { $0 }
4 Likes

I’ve been :exploding_head: for 3 months now.
Can’t believe that I’ve missed this for all these years. :ghost:

Thank you :pray: @cukr and @Lantua!

1 Like

I agree a well used ternary can be more readable than the alternatives. I’ve alternated between
let array = item != nil ? [item!] : []
and
let array = item.map { [$0] } ?? []

I’m not all that satisfied with either alternative. To me the map option is better than spreading the assignment over six lines, but it still creates more cognitive load than seems necessary. The force unwrap is probably more readable (but of course that still leaves a force unwrap), and works where map won’t here:
let optionalD = optionalA != nil ? optionalA!.optionalB : optionalC

However, I don’t think an implicit unwrap here is appropriate without going full Kotlin auto unwrap.

I wouldn’t mind seeing an expanded ternary to cover optionals and enums, perhaps with a keyword patterned after case let. For example:
let array = when let _item = item ? [_item] : []
or
enum E { case a(Int), b(Int) }
let array = when case let .a(_item) = e ? [_item] : []

Not that it's immediately obvious, but

let optionalD = (optionalA?.optionalB as Optional?) ?? optionalC

Why not?

let optionalD = optionalA.map { $0.optionalB } ?? optionalC
2 Likes

Hmm, I’m not sure if the compiler makes any guarantees about item being non-nil when it’s force unwrapped later. I don’t think even you can guarantee that, because there might be side effects:

class A {
    private var fooStorage: Int? = 1
    var foo: Int? {
        get {
            defer { fooStorage = nil }
            return fooStorage
        }
    }
    
    func printFoo() {
        print(foo != nil ? foo! : -1) // crash
        print(foo.map { $0 } ?? -1) // okay
    }
}

let a = A()
a.printFoo()

So I think the Optional.map approach is better.

2 Likes

Correct.

I’ve actually asked this before, and there is a theoretical window in here between the check and the force unwrap where multi-threaded programs or methods with side effects may update item. The only way to safely do this unless the variable is a local constant is to perform the unwrapping techniques the language gives you or use the mapping functions to actually process the value. These methods are guaranteed to handle the value as it is and return an appropriate value in one canonical check.

It’s unsafe to assume just because you performed a nil check before that it still holds later (despite how very close they are), unless you can guarantee that no one else can access or adjust the value in between. These unwrapping and map techniques are designed to make this safe for you.

The difference between
let optionalD = optionalA != nil ? optionalA!.optionalB : optionalC
and
let optionalD = (optionalA?.optionalB as Optional?) ?? optionalC
or
let optionalD = optionalA.map { $0.optionalB } ?? optionalC
is when optionalA == .some && optionalB == .none && optionalC == .some. In that case the result should be nil but instead it will be optionalC.

try it

2 Likes

That's a good point about side effects that I hadn't considered, although it's definitely an edge condition. However in that case the "when let" construct I suggested would be appealing.

What do you think of [item].flatMap { $0 }?

And I don't think when let is worth that much given that it already lose a lot of brevity.

Okay you're right, having tried it I retract my statement that it's not the same. I expected left to right evaluation, but apparently precedence around ?? isn't what I thought it was.

To me [item].flatMap { $0 } is hard for my C conditioned mind to grok without looking at it twice. I'm not sure I'd be comfortable putting it into code I expected someone else to maintain, but perhaps that's just my old style brain running smack into the new style functional style.

Since you didn't try both suggestions earlier, you gotta go timeout for 5-min before continuing... jk :stuck_out_tongue:.

The operator precedence, and associativity, can be found in this Operator Declarations. Anyway, don't see optional chaining? Well, it's a separated postfix expression, not ordinary postfix operator. Still, it is parenthesised like so:

((optionalA)?.optionalB) ?? optionalC

The problem here, is that optional chaining flattens the types for you by default, so a?.b is of type B?, not B??, and c is B?, which means that ?? uses this *overload with T == B. The lhs is nil when a or b is nil. And so it'll choose c whenever that happens.

Instead, if we annotate that a?.b as Optional? then it'll be of type B?? and the flattening doesn't happen. So instead they choose this overload with T == B?. In this case, lhs is nil only when a == nil.

@cukr achieve similar thing as map since it doesn't flatten the optional for you. I'd even suggest map method since to use as effectively at this level requires a lot of nuance interactions with type checker.

* Even if I say overload, they're just functions (operators) of the same name. Don't even think about getting it to be dynamically chosen at runtime. I don't even think as works with multiple chaining.

You right, I've taken my timeout. Thanks for the explanation. As you say, I wasn't accounting for optional flattening correctly. My expectation was that ?? would automatically flatten the operand, but it's not something I consciously considered.

1 Like

Regarding the operations that flattens, IIRC they are ?. (optional chaining), as?, and recently try?. Usually that's the right behaviour since you don't really care about the difference between .none and .some(.none).

Note:
Just to be clear ?? doesn't actually do the flattening job (though, in a sense, it does remove upto one ?). It's just another infix operator, defined within Optional, with two overloads.

3 Likes