3

I've got the following code, which illustrates a problem I haven't figured out how to solve cleanly, that is:

How can I make a function (isNil) that will return true for both nil, and Optional(nil), but false for anything else?

class Foo {
  var baz : Date? = nil
  subscript(key: String) -> Any? {
    get {
      let m = Mirror(reflecting: self)
      for child in m.children {
        if (child.label == key) { return child.value }
      }
      return nil
    }
  }
}

// this works unless the field is an Optional(nil)
func isNil(_ field: Any?) -> Bool {
  if field == nil { return true }
  return false
}

// this sort of works in a really terrible hacked way
func isNilViaString(_ field: Any?) -> Bool {
  if field == nil { return true }
  return "\(field.debugDescription)" == "Optional(nil)"
}

// this returns true as expected
print("isNil(nil) = \(isNil(nil))")
var optionalNil = Foo()["baz"]
// I'd like this to return true as well
print("isNil(optionalNil) = \(isNil(optionalNil))")
// this returns true, but is super hacky
print("isNilViaString(optionalNil) = \(isNilViaString(optionalNil))")
// this is an example of a problem with the isNilViaString method
print("isNilViaString(optionalNil) = \(isNilViaString("Optional(nil)"))")

Kem Mason
  • 1,548
  • 17
  • 26
  • I’m curious, how would you want to use this? There is this potentially similar question just yesterday https://stackoverflow.com/q/75174431/3141234 – Alexander Jan 20 '23 at 13:02
  • I'm using it as part of an object wrapper, where I want to be able to iterate over all the members of a class and do stuff with them -- one of those things is comparing to another value, that may be nil, and I want the Optional(nil) to match that case. Your answer on that question is interesting though, thanks for the link :) – Kem Mason Jan 20 '23 at 22:30

4 Answers4

3

isNil is best based on flattening the optionality of the wrapped value. (You may not actually have a use for isNil if you incorporate this directly into your subscript.)


If you don't care about the unwrapping failure details:

public extension Any? {
  /// Represent an `Optional` with `Any?` instead of `Any`.
  ///
  /// If `any` is an optional, this instance will copy it.
  /// Otherwise, this instance will wrap it.
  ///
  /// - Note: Use this to avoid an `Any?` actually representing an `Any??`.
  init(flattening any: Any) {
    switch any {
    case let optional as Self:
      self = optional
    }
  }

  var isNil: Bool { flatMap(Self.init) == nil }
}
subscript(key: String) -> Any? {
  ( Mirror(reflecting: self).children
    .first { $0.label == key }?
    .value
  ).flatMap(_?.init)
}

If you do:

public extension Optional {
  /// Represents that an `Optional` was `nil`.
  struct UnwrapError: Error & Equatable {
    public init() { }
  }
}

public extension Any? {
  /// The wrapped value, whether `Wrapped` is an `Optional` or not.
  /// - Throws: `Any?.UnwrapError` when `nil`,
  ///   or  `Any??.UnwrapError` when wrapping another `Optional` that is `nil`.
  var doublyUnwrapped: Wrapped {
    get throws {
      switch self {
      case let doubleWrapped?? as Self?:
        return doubleWrapped
      case _?:
        throw Self?.UnwrapError()
      case nil:
        throw UnwrapError()
      }
    }
  }

  var isNil: Bool { (try? doublyUnwrapped) == nil }
}
subscript(key: String) -> Any {
  get throws {
    try (
      Mirror(reflecting: self).children
        .first { $0.label == key }?
        .value
    )
    .doublyUnwrapped
  }
}
  • I love incorporating the solution directly into the subscript -- I'm fairly new to Swift though, and don't fully understand the code -- specifically the non-throwing code. I'm not clear on what the \_? does -- and is there a way to represent that code with an explicit closure instead of flatmap(\_?.init) -- also the init, what happens in the case where any is not an Any? -- or maybe I'm not even asking that correctly. I'm also a bit worried about monkey patching and breaking a 3rd party library (or SwiftUI or something) that relies on the Any??. I very much appreciate your help, thanks :) – Kem Mason Jan 20 '23 at 22:10
  • Maybe a better way of asking part of that is this: "what happens to the `any` argument that gets passed in when it doesn't match the case statement - is it just discarded or is it somehow assigned to self implicitly or what?" – Kem Mason Jan 20 '23 at 22:20
  • `_` is just a [type placeholder](https://github.com/apple/swift-evolution/blob/main/proposals/0315-placeholder-types.md). You could also use `Optional` instead of `_?` (matching the `Self.init` above, where `Self` is equivalent to `Optional`), or fully qualify it with `(Any?.init(flattening:))`. Do not create a new closure though; `init`s are already closures. –  Jan 21 '23 at 01:28
  • There is no possibility of the `any` not matching that `case`. It either matches directly as an `Optional` (`any` is `Any?`), or it gets promoted to be one (`as Optional` works for every type). –  Jan 21 '23 at 01:34
  • 1
    Thanks again, I understand better now, and love your solution, I'm adapting it for my code. Also, correct me if I'm wrong, but it seems like there's no danger of breaking anything else that might be using Any?.init because this init will never match any other calls, at least in part due to the flattening keyword. I'm loving Swift, so thanks again for helping me understand it better. – Kem Mason Jan 23 '23 at 19:01
2

You can convert them to AnyObject then compare with NSNull:

func isNil(_ field: Any?) -> Bool {
    return field as AnyObject is NSNull
}
Ranoiaetep
  • 5,872
  • 1
  • 14
  • 39
1

Once you check if field is nil or not, you can then use a switch with a case that checks if the value is an optional.

Here is an updated isNil method:

func isNil(_ field: Any?) -> Bool {
    if let field {
        switch field {
        case let optional as Optional<Any>:
            if case .some = optional {
                // This was an Optional but not nil
                return false
            } else {
                // This was an Optional<nil>
                return true
            }
        default:
            // This was not an optional
            return false
        }
    } else {
        // nil was passed in
        return true
    }
}

Some updated test cases:

print("isNil(nil) = \(isNil(nil))")

var optionalNil = Foo()["baz"]
print("isNil(optionalNil) = \(isNil(optionalNil))")

var foo = Foo()
foo.baz = Date()
var optional = foo["baz"]
print("isNil(optional) = \(isNil(optional))")

print("isNil(Date()) = \(isNil(Date()))")

Output:

isNil(nil) = true
isNil(optionalNil) = true
isNil(optional) = false
isNil(Date()) = false

HangarRash
  • 7,314
  • 5
  • 5
  • 32
-1

This might help you. But this will work only for nil and Optional(nil). If optional will have any value then it will not work.

func isOptional(_ instance: Any?) -> Bool {
  if instance == nil { return true }
  if let ins = instance {
    let mirror = Mirror(reflecting: ins)
    let style = mirror.displayStyle
    return style == .optional
  }
  return false
}

And the results will be:

let a: Int = 1  // "1\n"
let b: Int? = 2 "Optional(2)\n"
let c: Double = 3.0 // "3.0\n"
let d: Double? = 4.0 // "Optional(4.0)\n"
let e: NSString = "Hello" // "Hello\n"
let f: NSString? = "Hello" // "Optional(Hello)\n"
let optionalNil = Foo()["baz"] // "Optional(nil)\n"


isOptional(a) // fasle
isOptional(b) // true - warning
isOptional(c) // false
isOptional(d) // true - warning
isOptional(e) // false
isOptional(f) // false
isOptional(optionalNil) // true
isOptional(nil) // true

Originally answered by Kaz Yoshikawa. I just modified it as per your requirement.

DD7
  • 11
  • 1
  • 3