Optional chaining on prefix operators

There is an asymmetry when it comes to optional chaining with prefix and postfix operators:

func foo(a: Int?) -> PartialRangeFrom<Int> {
    return a?... ?? 0... // works
}

func bar(b: Int?) -> PartialRangeThrough<Int> {
    return ...b? ?? ...0 // doesn't work
}

bar(b:) can be rewritten as

func bar(b: Int?) -> PartialRangeThrough<Int> {
    return ...(b ?? 0)   // works
}

which works.

What does everyone think? Should Swift add support for optional chaining on prefix operators?

1 Like

Is... that even supposed to work in the first place? That's weird. I would expect it to only work on instance member accesses, which operators aren't (they're static or global functions).

You can't do this with infix operators either; a? + b doesn't work for a: Int? and b: Int.

IMO, (a ?? 0)... and ...(b ?? 0) are significantly more readable than a?... ?? 0... and ...b? ?? ...0 anyway.

2 Likes

Optional chaining is used for subscripts and function calls too (foo?[a], foo?()), so the easiest way to make it general was to make it a postfix expression on its own. That means it slots into the grammar in the usual order of operations, which is "postfix, then prefix, then infix"…except for optional-chained assignments, which are special.

I'm personally not interested in making prefix operators optional-chain-able, even though it creates an asymmetry for ..., just because I find it hard to read…but it does fit into the grammar as long as you're ending with the ?.

2 Likes

I guess I would have expected the compiler to flag it as an error (for consistency with prefix and infix operators) if postfix-? was followed by another postfix operator. Do you remember if that was intentional or just an oversight?

I imagine it might be too late for source compatibility reasons to make it an error now if there are any libraries relying on this behavior. (I don't personally know of any, though.)

In the examples using range formation operators, I agree that (a ?? 0)... and ...(b ?? 0) are more readable than a?... ?? 0... and ...b? ?? ...0.

However, ...b? ?? ...0 style might be better in some other situations. For an extreme and silly example:

struct Example {
    let a: Int
    let b: Int
    let c: Int
    /* more instance properties... */
    let y: Int
    let z: Int
}

prefix operator ¿¿¿: ExamplePrecedence

extension Example {
    static func ¿¿¿ (value: Int) {
        return Self(
            a: value,
            b: value,
            c: value,
             /* more instance properties... */
            y: value,
            z: value
        )
    }
}

extension Example {
    static let oneToTwentySix = Self(
        a: 1,
        b: 2,
        c: 3,
        /* more instance properties... */
        y: 25,
        z: 26
    )
}

It would be easier to write ¿¿¿someOptionalInt? ?? .oneToTwentySix than alternatives in this case.

Do you have a use case that isn't an artificially concocted example? I'm not sure what that API is meant to represent, with or without optional chaining on the prefix operator, other than something that shows that a few characters of typing can be saved. The bar that changes need to meet tends to be higher than that.

There's been several discussions about adding some sort of support for the operation of "call function f only if arguments a, b, ... are non-nil" (most recently, I believe, here).

The ultimate goal being to allow something like:

func foo(arg: String) { ... }

let str: String? = maybeGetAString()
foo(str?) // evaluate foo only if str is non-nil

I would hope that any attempt to address the issue raised in that thread would naturally extend to operators as well.

I do have an actual use case, but I think it's a little harder to explain:

It involves an Interval type I built to model mathematical intervals. The type has operators for creating all kinds of intervals, except for the unbounded ones.

For example, 5≤∙∙ creates an interval that's left-closed and -bounded at 5 and right-open and -unbounded; ∙∙≤10 creates an interval that's left-open and -unbounded and right-closed and -bounded at 10. Interval<Int>.unbounded creates an unbounded interval of integers.

In the use case I linked above, the program is a mod manager for a game, and it reads some version strings from a JSON object in order to figure out dependencies between mods. The dependency could be either an exact version number, or a combination of minimum or maximum versions. The version information could be absent, in which case any version is acceptable.

I modeled all the different versions like this: [exact version, exact version], [minimum version, +∞), (-∞, maximum version], [minimum version, maximum version], (-∞, +∞). If either minimum version or maximum version is absent (nil), then it is assumed to be unbounded. In code, it becomes:

let minimalVersion = try container.decodeIfPresent(CKANMetadataVersion.self, forKey: .minimalVersion)
let lowerBoundedVersions = minimalVersion?≤∙∙ ?? .unbounded
let maximalVersion = try container.decodeIfPresent(CKANMetadataVersion.self, forKey: .maximalVersion)
let upperBoundedVersions = ∙∙≤maximalVersion? ?? .unbounded // Optional chaining doesn't work here.
let versions = lowerBoundedVersions ∩ upperBoundedVersions

^ line 4 doesn't work because prefix operators can't be optional chained, so it's written as this instead:

var upperBoundedVersions = Interval<CKANMetadataVersion>.unbounded
if let maximalVersion = try container.decodeIfPresent(CKANMetadataVersion.self, forKey: .maximalVersion) {
    upperBoundedVersions = ∙∙≤maximalVersion
}

You could use Optional.map here instead:

let minimalVersion = try container.decodeIfPresent(CKANMetadataVersion.self, forKey: .minimalVersion)
let lowerBoundedVersions = minimalVersion.map { $0≤∙∙ } ?? .unbounded
let maximalVersion = try container.decodeIfPresent(CKANMetadataVersion.self, forKey: .maximalVersion)
let upperBoundedVersions = maximalVersion.map { ∙∙≤$0 } ?? .unbounded
let versions = lowerBoundedVersions ∩ upperBoundedVersions

This may be subjective, but I think that reads more clearly because it makes the fact that those values are optionals stand out, rather than burying the possibility with a single ? that gets easily lost among the other punctuation of the operators.

1 Like

Given the error that comes from this code, I guess it's an oversight:

let x: Int?? = 5
print(x?) // error: '?' must be followed by a call, member lookup, or subscript

In earlier (publicly released!) versions of Swift, this flattened one level of optional, as accidental emergent behavior from the way multiple postfix ? get merged together (equivalent to Optional.flatMap). At some point we decided to ban that (it's better written as x ?? nil), and given this error message it sounds like that wasn't intended to include postfix operators.

However, these days I wonder what's the difference between a postfix operator and a call or subscript that means the rules should be different, and I did think of this silly example that uses another no-longer-valid piece of Swift: counter?++. Postfix operators just don't seem to be that popular, though, except for variations of ranges (like @wowbagger's) and of course postfix !. That makes it hard to evaluate changes here.

Yep.

1 Like

This is a good advice. I didn't know about Optional.map previously.

I agree. maximalVersion.map { ∙∙≤$0 } ?? .unbounded is easier to read than ∙∙≤maximalVersion? ?? .unbounded, especially with the 3 consecutive ? in the latter.

1 Like