Bug: Generic type cannot check for nil

Encountering a bug with Swift 5.5:

func isNil<T>(_ value: T) -> Bool {
    value == nil
}

Always returns false even when nil is passed in.

Please let me know if this is a known bug or if I should file a report.

Thanks

Seems expected to me. If you pass a nil optional, say Optional<String>, your comparison becomes Optional<String>.none == Optional<Optional<String>>.none. Clearly this is never true. If you want to handle optionals you need a separate method which takes T?.

2 Likes

Not a bug. You're comparing a value of type T to Optional<T>.none. If you pass in a value of nil, then T is an optional type (that is, Optional<U>, for some type U), and Optional<U>.none is not equal to Optional<T>.none, aka Optional<Optional<U>>.none.

Ok

That doesn't work either. It never gets called because we're calling it from another function which itself has a generic type property, which is unavoidable because it's a property wrapper's accessor:

@propertyWrapper struct Foo<Bar> {
   var wrappedValue: Bar { ... }
}

Why does Swift make it impossible to simply check if some particular variable contains nil?

I think this seems brokenly idiotic and should be fixed. We should not have to make two implementations of everything just because Swift refuses to check whether a variable is equal to nil unless it statically knows the type is optional.

The only decent workaround I've found is to do this:

if "\(value)" == "nil" { ... }

But that's an ugly hack and the language should not force us into ugly hacks to avoid odious code duplication.

No, it doesn't. The comparison remains .none == nil no matter how many layers of optional nesting exist in the type.

Comparing the same exact value to nil outside the context of a generic function always works fine. Based on Liksov's Substitution Principle then it should also work fine inside the generic context.

The only other sane solution would be for optionals to not be allowed to be passed into generic params that have a different level of optionality than the thing being passed in, which clearly we're not prepared to do.

Another problem is that Swift insists on type-erasing the optional wrapping and then not allowing a cast back to an optional or dynamic or a static dispatch to a function with an optional generic parameter to unerase the optionality (we did try dynamic function / subscript to no avail).

Even worse if you end up with an array of type Array<T>, you can't even do a compactmap to get rid of the nil values in the array, and when you get elements out of it, you cannot do a simple guard let to check dynamically for a nil where you know one might exist.

I would argue we should have a way to dynamically check for nil in contexts where the static optionality has been lost.

That’s no different than saying I should be able to check value == “string” no matter the context. Optional is just a type in Swift, nothing special. The only special thing about it are the affordances the language has to make it easier to work with.

You're arguing like a language-lawyer and not a language user.

value == "string" can be done as (value as? String) == "string", and that will fail as expected if value isn't a String.

There is no equivalent for testing possible Optionals because nil isn't any specific type. i.e. writing (value as? Optional<String>) == nil could fail because value isn't nil or because it's not a String?. But you can't even write that in a generic context, because what do you put as the RHS of that comparison?

A strange response. Optional still isn’t special here. This cast isn’t possible with most generic types. value == [] isn’t possible either. I’m not sure what the feature would be called that you’d need. Partial type casting? Something like (value as?Optional<_>) == Optional<_>.none or something like that.

Even as a language user, "checking for nil" is somewhat ambiguous because optionals can be nested.

The simple case is plain optionals, such as Optional<String> aka String?: it is nil, or not.

But consider Optional<Optional<String>> aka String??: it can contain a string .some(.some("foo")), or two kinds of nil: .none and .some(.none).

Add more levels of optionality, and you have even more "kinds of nil".

The game here is to understand what the OP is after, which "kind of nil" has to be checked for.

Is it only dealing with plain single-level optionals? In this case, there is no need for a isNil function: value == nil is just enough, and there's not much any question.

Since there is a question, I guess we're talking about "flattening nested optionals", or "deep unwrapping of optionals". There are multiple threads about this topic in this forum, and one I can remember is Challenge: Flattening nested optionals.

And if this guess is wrong, then I advise @vitamin to tell what is, exactly, the initial problem to solve. Focusing on the isNil function, much likely a by-product on the way to solve the initial problem, is probably a loss of time for everybody.

This is not a bug and the compiler also rightfully warns you about it: "Comparing non-optional value of type 'T' to 'nil' always returns false"


Tested with Swift 5.5.2 (Xcode 13.2.1) and Swift 5.6 (Xcode 13.3 Beta 1)

1 Like

That's a very unhelpful response. The OT already knows about this. It's literally what the post is about. The problem is that T could be Optional, which means it should be comparable to nil. The compiler-acceptable method for dealing with this is completely non-obvious.

I don't think this framing helps the conversation much either. T certainly could be Optional. It could also be String, or Int, or ByteBuffer, or SelectableChannel, or LazyVStack, or any of the other infinity of types in the program. Should a generic type be comparable to arbitrary values of other types? I think in general the Swift community agrees that the answer is no.

The problem is that you cannot cast to Optional, because that is an insufficiently specified type. Much like Go, Swift has typed nil. nil looks like a single unitary value, but it is not: there is one nil per possible type, an infinity of nils.

In this context, what does it mean to compare a value of type T to nil? Which nil? Swift is only aware of one nil in this context, which is the nil for Optional<T>, so that's what it assumes this means. It performs implicit optional promotion to the left hand side of OPs expression and so compares Optional<T>.some(value) to Optional<T>.none and concludes they are different.

I think it's fair to summarise the OP's ask as: "I would like a way to know an unconstrained generic type T is of the form Optional<U>, without having to only accept types of the form Optional<U>." This is totally possible: it's just code, just engineering. But it's not possible without a change to Swift.

Because we cannot know ahead of time what values this function will be called with, we'd need to use the only information we have: the value witness table for T. This would essentially need an entry (that does not exist in Swift today) that provides a way to load the representation for Optional<T>.none at runtime. This function pointer can be NULL for types which are not Optional, and so we can clearly distinguish the difference.

Swift could totally have done this! But it did not, and it would probably be ABI breaking to do it now, and in my personal opinion the utility it adds is fairly minimal.

5 Likes

This is, of course, possible.

protocol OptionalProtocol {
  var isSingleLevelNil: Bool { get }
  var isDeepNil: Bool { get }
}

extension Optional: OptionalProtocol {

  var isSingleLevelNil: Bool {
    self == nil
  }

  var isDeepNil: Bool {
    switch self {
    case .none:
      return true
    case .some(let value as OptionalProtocol):
      return value.isDeepNil
    case .some(_):
      return false
    }
  }
}


let singleLevelOptional: Optional<Int> = 42
print(singleLevelOptional.isSingleLevelNil) // false
print(singleLevelOptional.isDeepNil) // false

let singleLevelOptional2: Optional<Int> = nil
print(singleLevelOptional2.isSingleLevelNil) // true
print(singleLevelOptional2.isDeepNil) // true

let nestedOptional: Optional<Optional<Optional<Optional<Int>>>> = 42
print(nestedOptional.isSingleLevelNil) // false
print(nestedOptional.isDeepNil) // false

let nestedOptional2: Optional<Optional<Optional<Optional<Int>>>> = nil
print(nestedOptional2.isSingleLevelNil) // true - this is .none(.none(.none(....)))
print(nestedOptional2.isDeepNil) // true

let nestedOptional3: Optional<Optional<Optional<Optional<Int>>>> = .some(.some(.some(.none)))
print(nestedOptional3.isSingleLevelNil) // false
print(nestedOptional3.isDeepNil) // true

If you have an unconstrained type T, you can downcast to OptionalProtocol and check isSingleLevelNil or isDeepNil, depending on the precise semantics you're looking for.

You need a protocol because plain Optional (with its generic parameters left unspecified) is not a type you can instantiate. The same applies to Array and Dictionary; AFAIK the only time you can use a generic type without specifying its parameters is when declaring protocol conformances. At all other times, you have to specify the parameters (Array<Int>, Array<T>, or Dictionary<String, T.SomeAssociatedType>).

If you want to define a common interface or add some members to all Optionals (or all Arrays, Dictionaries, etc), use a protocol.

It would be cool if we had some sort of generics existential - something like an Optional<?> or Array<?>, but you can work around it pretty easily as demonstrated above. It would basically work like a compiler-generated protocol.

I use this trick in my own code. My sympathy with the OP lies in the fact that this solution is a very non-obvious replacement for x == nil.

1 Like

Sure, but I don't think that's a problem with optionals - that's just generics in general. The same applies to nested arrays, dictionaries, etc.

Writing generic code requires you to think more abstractly than writing code which uses concrete types. Generally, it's good to add constraints at as high a level as possible to define the interfaces your code requires, but if you can't do that, you'll have to try casting to an appropriate interface.

(apologies if this was already explained upthread and I missed it)

It might help to understand why this compiles and why it's false, which is a lot of what is causing the confusion. Ideally it wouldn't compile at all because T is not optional. But it does, because we do implicit optional conversion. This code is actually:

func isNil<T>(_ value: T) -> Bool {
    let tmp = Optional<T>.some(value)
    return tmp == nil
}

You can never compare an arbitary T to nil because it isn't optional (just like you can't compare to T to "foo") but the compiler implicitly wraps the value in an optional for you, and then it works. In general implicit optional promotion is essential to the smooth operation of the language, but it does lead to some pretty unfortunate bear traps like this.

5 Likes

This is closely related to Generic types as generic constraints.

Indeed, if every generic type had an implicit protocol that abstracted all the capabilities of the type (or at least if things behaved as though there were such a protocol), then the idea proposed in that thread would work directly, without any extra complexity.

You could write, eg, extension Dictionary where Value: Optional, and it would work as expected.

And for this thread, once SE–03039: Unlock existentials for all protocols lands, then implicit protocols for generic types would make it possible to write this:

extension Optional {
  var isNil: Bool { self == nil }
}

func isNil<T>(_ value: T) -> Bool {
  return (value as? Optional)?.isNil ?? false
}

Or even just this, without extending Optional at all:

func isNil<T>(_ value: T) -> Bool {
  guard let x = value as? Optional else { return false }
  return x == nil
}

(That == is the _OptionalNilComparisonType version, because there is no Equatable constraint.)

1 Like

I wish this was the case.

I wish Swift had less of these magic convenience features/exceptions. If implicit optional conversion had not existed and if we used .none instead of nil, then the code wouldn’t compile and the programmer would more clearly see a more consistent set of fewer rules.

Out of curiosity and at the risk of getting off topic: What are some examples of how they are essential? Maybe there are threads discussing this, ie whether conveniences that introduces inconsistencies/confusion is worth adding to the language or not?

Without optional promotion you would have to wrap a lot of things in optionals. It would happen constantly and be intensely annoying:

takesOptionalArgument(.some(x))
myDictionary[key] = .some(x)
let x: Int? = .some(1)
if x == .some("foo") {

This is essential sugar, there's no question in my mind it's needed. The question is, can that sugar be narrowed slightly so the above continues to work, but this case did not. It's a tricky needle to thread.

This is conflating two things I think. nil and .none are synonyms.

And do be fair the code in the OP does produce a warning:

comparing non-optional value of type 'T' to 'nil' always returns false

This thread is really more about wanting different semantics from the language, so even once the reason it's false are understood, the desire for it to behave differently remains (and while desired by this user, isn't probably a change for the better).

5 Likes

I meant why have nil at all when there’s already a less magic .none that would lead a new programmer to the actual types and concepts involved. nil is just … I don’t know, something that might trick people into thinking along the lines of null in other languages?