Given that an optional type can be represented by a non-optional generic parameter, how can you tell the difference between Optional<T>.none and nil?

Consider a property wrapper that has a default and an override value, where wrappedValue will return default unless override is specified, in which case it will return override instead:

@propertyWrapper
struct Foo<B> {
    typealias Bar = B
    
    /// Default value to use unless `override` is specified.
    var `default`: Bar
    
    /// A value to use instead of the default.
    var override: Bar?
    
    init(override: Bar? = nil, default: Bar) {
        self.default = `default`
        
        guard let override = override as? Bar else {
            self.override = nil
            return
        }
        self.override = override
    }
    
    var wrappedValue: Bar {
        guard let override = override as? Bar else {
            return `default`
        }
        return override
    }
}

Now when I use @Foo to wrap an optional parameter, it never returns the default value even when I don't specify an override:

struct Qux {
    @Foo(default: 42) var optionalFoo: Int?
    @Foo(default: 42) var nonOptionalFoo: Int
}

let qux = Qux()

print("\(qux.optionalFoo)") // always prints "nil"!
print("\(qux.nonOptionalFoo)") // prints 42 as expected

To me this seems like a bug or issue with Swift.

Since no override is specified in @Foo(default: 42) var optionalFoo: Int?, therefore optionalFoo should be 42, not "nil". It should only be nil if the wrapper is specified like @Foo(override: nil, default: 42). But of course "nil" has to be the default value of "override" for times when no override is specified, so it's like a catch-22.

What is the solution here? How can I represent override so that in case the generic parameter Foo.Bar is actually an optional, then I can make override be able to either be nil (no override) or Optional<Bar>.none (override the default and use nil instead)?

I was able to achieve the desired behavior by changing the guard in wrappedValue to:

        guard let override = override else {
            return `default`
        }

Interestingly, the original formulation gives me a "Conditional downcast from B? to B does nothing" error. I don't think that error makes sense when the result of the cast is used as the right-hand side of an if let or guard let binding, unless I'm missing something...

What version of Xcode or Playgrounds are you using?

On both Swift 5.1 Mac Playgrounds app, and on Xcode 12.2 beta 3 playgrounds, I get the same result, regardless of whether I use your version of the guard statement in wrappedValue.

Namely, print("\(qux.optionalFoo)") always prints "nil" instead of printing "Optional(42)" as desired.

However if I remove as Bar? from both guard statements in the original then now the behavior is as follows:

@propertyWrapper
struct Foo<B> {
    typealias Bar = B
    
    /// Default value to use unless `override` is specified.
    var `default`: Bar
    
    /// A value to use instead of the default.
    var override: Bar?
    
    init(override: Bar? = nil, default: Bar) {
        self.default = `default`
        
        guard let override = override else {
            self.override = nil
            return
        }
        self.override = override
    }
    
    var wrappedValue: Bar {
        guard let override = override else {
            return `default`
        }
        return override
    }
}

Now the results are:

struct Qux {
    @Foo(override: Optional<Int>.none, default: 42) var optionalFoo1: Int?
    @Foo(override: nil, default: 42) var optionalFoo2: Int?
    @Foo(default: 42) var optionalFoo3: Int?
    @Foo(default: 42) var nonOptionalFoo: Int
}

let qux = Qux()

print("\(qux.optionalFoo1)") // prints "nil" as expected
print("\(qux.optionalFoo2)") // prints "Optional(42)"
print("\(qux.optionalFoo3)") // prints "Optional(42)" as expected
print("\(qux.nonOptionalFoo)") // prints 42 as expected

print("\(Optional<Int>.none == nil)") // prints true... !

But... but... why should the following two have different values:

    @Foo(override: Optional<Int>.none, default: 42) var optionalFoo1: Int?
    @Foo(override: nil, default: 42) var optionalFoo2: Int?

... ???

... especially considering that Optional<Int>.none == nil returns true ... ?

PS: Optional<Int>.none === nil is a compiler error

Ah! Yes, I had done that too, and forgot to mention :man_facepalming: Apologies!

When the wrapped type of Foo is Int?, the type of the override argument is Int??. This means that Optional<Int>.none gets promoted to Optional<Optional<Int>>.some(.none). However, passing nil to an argument of type Int?? is the same as passing Optional<Optional<Int>>.none.

When comparing Optional<Int>.none == nil, both arguments are of type Optional<Int>, and so nil is a synonym for Optional<Int>.none. You can see that:

Optional<Int>.none == nil as Optional<Optional<Int>>

will return false.

Yes, this is to be expected. The === is only for checking object identity, meaning it only works for class/AnyObject constrained instances.

2 Likes

So what you're saying is:

 (nil as Int? as Int??  !=  nil as Int??) == true

... and this is expected, because LHS's value is Optional.some(nil), and RHS's value is Optional.none?

This seems like a bug in Swift.

In the documentation on Equatable, Apple says:

Since equality between instances of Equatable types is an equivalence relation, any of your custom types that conform to Equatable must satisfy three conditions, for any values a , b , and c :

  • a == a is always true (Reflexivity)
  • a == b implies b == a (Symmetry)
  • a == b and b == c implies a == c (Transitivity)

Moreover, inequality is the inverse of equality, so any custom implementation of the != operator must guarantee that a != b implies !(a == b) . The default implementation of the != operator function satisfies this requirement.

And yet ... given the following values:

let u: Int? = nil
let y = nil as Int? as Int?? 
let a = nil as Int? as Int?? as? Int?

... we can see that:

assert(y == a)  // passes
// but...
assert(u == a && u == nil && a != nil) // passes. wtf
assert(u == y && u == nil && y != nil) // passes. wtf

Therefore one of the following three statements is true:

  1. Apple's documentation is wrong, and this is not a bug
  2. Apple documentation is right, and this is a bug
  3. Apple's documentation is right, and this is not a bug, because Optional is an official type of the Swift language, and therefore, it's not among "your custom types that conform to Equatable" which "must satisfy three conditions" (including transitivity)

Which of these three is correct? Please let me know so that I can file the appropriate issue report.

None of these is correct. :slightly_smiling_face:

The behavior you're observing is due to a fourth option:

  1. nil does not represent a "value" as described in the documentation of Equatable. Instead, nil represents a literal, just like 0, "some string", or [a, b, c] (which are an integer, string, and array literal, respectively).

Semantically, literals are converted to a value of the appropriate type at runtime via the requirements specified in the appropriate ExpressibleBy*Literal protocol, which for ExpressibleByNilLiteral is the initializer init(nilLiteral:) (note: this particular literal protocol is really only meant to be used by Optional).

All of that is to say that in the examples you've provided, nil does not represent the same value in each clause.

E.g., with

assert(u == a && u == nil && a != nil)

The first nil is in a comparison u, which has type Int?, and so nil is of type Int?, resulting in the value Optional<Int>.none. OTOH, the second nil is in a comparison with a, which is of type Int?? (note: foo as? T produces a value of type T?), and so nil is of type Int??, resulting in a value of Optional<Optional<Int>>.none. As noted previously, this is not the same as Optional<Int>.none. Therefore, the three comparisons you've written do not represent a transitivity violation.

6 Likes

I'll grant you that transitivity should not apply in my first example, considering that the compiler implicitly casts the nil literal to a differing optional type in each case, if you'll grant me that transitivity should apply any time the nil literal gets implicitly cast to the same type.

And yet it doesn't:

let u: Int? = nil

assert(u == Optional<Int>.none) // passes
assert(u == nil)                // passes

// however...

func isNil<T: Equatable>(value: T) -> Bool {
    assert(value == (u as! T)) // passes
    return value == nil
}

assert(isNil(value: u))         // assertion fails! wtf

Why does the last assertion fail?

How is this not a major flaw in Swift?

I'm curious why Swift did not require that a generic type parameter must dictate the level of optionality of the types that can be passed in, so that guard let , if let etc. won't be able to do astonishing things when someone passes in an optional to an (apparently) non-optional generic parameter.

Yes. When nil represents a value of type Optional<T>, it always represents the same value Optional<T>.none, which will obey transitivity.

So, there's several things going on here:

  • As already mentioned, nil is a literal which, when it has type Optional<T>, represents the value returned by Optional<T>.init(nilLiteral: ()) (which will be Optional<T>.none).
  • Casts like as! and as? are able to implicitly flatten optionals.
  • Values of type T can be implicitly promoted to type T?.

When you hit this line:

return value == nil

all the compiler knows is that value has type T, which conforms to Equatable. In particular, it doesn't know whether T is optional or not. On the other hand, nil is known to be of type Optional<U> for some type U. The compiler will attempt to substitute T for U, implicitly promote value to type Optional<T>, and see if value == Optional<T>.none.

Notably, since value is of type T, this comparison will always be false, because when value is promoted to type Optional<T>, it has value .some(value).

Now, when you call isNil(value: u), you conceptually pass in Int? for the type parameter T, so you again have the issue where you're comparing .some(.none) to .none.

2 Likes

Take a look at your generic function:

func isNil<T: Equatable>(value: T) -> Bool {
    // <snip>
    return value == nil
}

You've only specified the constraint T: Equatable. Note that T itself does not conform to ExpressibleByNilLiteral. Therefore, the type of nil in the expression value == nil has to be Optional<T>.

You then invoked the function with a value of type Optional<Int> and a value of Optional<Int>.none. So, T in this case is Optional<Int>, and therefore the type of nil on the right hand side of the comparison must be Optional<T> == Optional<Optional<Int>>, and the value therefore is Optional<Optional<Int>>.none.

So then you are asking: is Optional<Int>.none equal to Optional<Optional<Int>>.none? There is no such heterogeneous comparison operator. Instead, we must implicitly wrap the left hand side. Now, is Optional<Optional<Int>>.some(.none) equal to Optional<Optional<Int>>.none? Clearly not.

3 Likes

I'll also add that the current behavior is what it is precisely so that generic functions and types do not behave in unexpected ways when optionals are passed in for generic parameters. Notably, with Swift generics, code must properly typecheck in the generic context, not only at the point of instantiation (as in, say, C++).

This is why you can't write something like:

func isNil<T>(value: T) -> Bool {
    if let _ = value { // error: initializer for conditional binding must have Optional type, not 'T'
        return false
    } else {
        return true
    }
}

The "proper" way to write this function (depending on the precise semantics you want) is likely closer to this:

func isNil<T>(value: T?) -> Bool {
    if let _ = value {
        return false
    } else {
        return true
    }
}

let x: Int = 5
let y: Int? = 6
let z: Int? = nil

isNil(value: x) // false
isNil(value: y) // false
isNil(value: z) // true
isNil(value: x as Int??) // false
You've only specified the constraint T: Equatable. Note that T itself does not conform to ExpressibleByNilLiteral. Therefore, the type of nil in the expression value == nil has to be Optional<T>.

Rather than going ahead and implicitly assuming that nil should be Optional<T>.none, the compiler should throw an error and require that T must conform to ExpressibleByNilLiteral for the comparison to be valid. Clearly T is T not, Optional<T> so this inference makes no sense at all.

Yeah, why should nil be equal to nil? That would make no sense at all... unless oh wait:

assert(nil == nil) // passes
assert(Optional<Optional<Int>>.some(.none) == Optional<Int>.none) // passes

assert(nil == Optional<Int>.none) // passes

assert(Optional<Optional<Int>>.some(.none) == nil) // fails

This seems very broken. I get that, y'know, the compiler is currently programmed to make hidden assumptions to save developers from having to be explicit about which kind of nil they mean, exactly.

However this does not obey the principle of least astonishment.

Developers expect that nil means nil, and transitivity should apply to it.

[quote="Jumhyn, post:12, topic:41551"]
I'll also add that the current behavior is what it is precisely so that generic functions and types do not behave in unexpected ways when optionals are passed in for generic parameters.[/quote]

What's the best way to simply not allow optionals to be passed into non-optional generic parameters?

Having a scenario where a generic type might be optional seems bad, because something that might be optional might as well be optional.

You've got a point here: it seems to me there should be a warning for that comparison.

func isNil<T: Equatable>(value: T) -> Bool {
  return value == nil // warning: `value == nil` will always be `false`, because `value` is promoted to T?
}

You’ve certainly hit a quirk of the type system, and I agree that it may seem counter intuitive. But I think the opposite, that is not having optional promotion, would be just as astonishing, if not more so.

Just imagine not being able to pass an honest int where an Int? is expected, because of a type mismatch? Or not being able to compare optional variables against honest values?

But, I agree that the return value == nil Lind should produce a warning, stating that it will always be false due to optional promotion.

These are not the same thing, for the reasons already enumerated above.

Optional<Int>.none, like any other value, can be compared to a value with one additional "layer" of optionality because it can be wrapped. When you do so, you get Optional<Optional<Int>>.some(.none), which is clearly equal to the left hand side.

As has been explained, nil is a literal, and like every literal, it has no specific type (and therefore no specific value) independent of a specific expression. There is no such thing as "transitivity" when there is not a single value to speak of. If a developer has that expectation that you state about literals, then they will be continue to be surprised.

1 Like

That's a fair enough warning; I think we have discussed this before here where the compiler can probably usefully warn whenever it can determine that some nontrivial expression will always evaluate to the same result.

There is no way in Swift to declare that a generic type is constrained not to be something, and that applies to Optional just like any other type. (You can search the forums for an explanation of why.) You can always write that something is of type T? as demonstrated above, which can be useful for certain use cases.

As soon as you start dealing with nested optionals, Swift definitely has some ergonomic issues. However, this is a deliberate tradeoff—nested optionals are exceedingly rare, and single optionals are widely used. It's reasonable for Swift to optimize for a single level of optionality at the expense of nested optionals.

I describe the issues with nested optionals as "ergonomic" because there's nothing that prohibits you from working with them. Swift APIs will happily traffic in nested optionals, and there's circumstances where a double optional makes perfect sense as a way to model a problem (like, say, distinguishing between "unset" and "set to nil" states, as in your first post in this thread).

However, due to the potential for confusion between the different possible levels of optionality, I would pretty much always push back against any proposed API design which made use of nested optionals. It will save you many headaches to more explicitly model your relationships from the start, e.g., by creating an enum

enum PropertyState {
  case unset
  case set(Int?)
}

For better or for worse, this is simply the wrong way to think about literals in Swift. Again, it works well enough for a single level of optionality, but breaks down as soon as you have nested optionals. If you're dealing with different types, nil means something different. It's the same as this:

let x: Float = 16777217
let y: Int = 16777217
print(Int(x) == y) // false!

You could say "Developers expect that 16777217 means 16777217," but that's clearly ridiculous in this example. The literal 16777217 only "means" anything in the context of a particular type, and in the context of Float, 16777217 "means" the same thing as 16777216.0.

I question the premise that this is a valuable end. There are likely better ways to achieve your goals.

Ah, but if you have a variable x of some unconstrained generic type T, it is the case that x always has a value of type T. Effectively, you may assume that T is not optional, because that's not your concern as the author of a generic function. Clients may pass in an optional type, and your generic code will treat it the same as any other type.

ETA: IMO, if this remains confusing for you, your best bet is probably to avoid nil in situations where you are dealing with nested optionals. Write out Int??.none or Int??.some(Int?.none) explicitly to make it clear what values you're trying to represent.

based on this, then

nil == nil
nil != nil

should not compile? But they do compile

This is because the standard library has built in overloads for == and !=:

public func == (t0: Any.Type?, t1: Any.Type?) -> Bool
public func != (t0: Any.Type?, t1: Any.Type?) -> Bool

So nil in those expressions end up representing Optional<Any.Type>.none. If you put nil somewhere where it really is divorced from any type context, you'll get an error:

let x = nil // error: 'nil' requires a contextual type
1 Like
Terms of Service

Privacy Policy

Cookie Policy