Unexpected return type from keyPath subscript

(Joanna Carter) #1

Like many, I use the subject[keyPath: keyPath] syntax to extract values from an object.

However, testing for an optional value being nil or not was getting to be problematic.
e.g.

struct Person
{
  …

  var age: Int?
}

let person = Person(age: 21)

let age = person[keyPath: \Person.age]

It turns out that the subscript returns an Optional that contains an Optional. When the outer Optional contains a value, it actually contains an Optional<Any>, whose .some case contains an Optional<Int>, whose .some case contains an Int.

What I find more peculiar is that you can end up with an Optional<Any> where the .some case can contain nil !

Therefore, you end up having to write convoluted code such as :

      if case .some(Optional<Any>.some(let v)) = value

… just to extract the inner value.

This may have been intentional but it is quite confusing and took a fair bit of effort to discover why testing the "outer" optional could never simply be tested for nil.

Could someone please explain the rationale behind this design and whether I am missing something simple?

Challenge: Flattening nested optionals
(Martin R) #2

I cannot reproduce that behavior (Swift 5, Xcode 10.2.1):

struct Person {
    var age: Int?
}

let person = Person(age: 21)
let age = person[keyPath: \Person.age]

print(age) // Optional(21)
print(type(of: age)) // Optional<Int>

The subscript returns an Optional<Int>, which is the type of the age property. Or did I misunderstand something?

2 Likes
(Joanna Carter) #3

I'm sorry, I should have made the whole situation clearer. There is no problem when you know the exact type of the key path; the problem arises if you keep the key paths in a dictionary, for use in a "generic" situation where you do not know the real type of the key path, only that it is an AnyKeyPath

    let keyPaths: [String : AnyKeyPath] = ["name" : \Person.name, "age" : \Person.age]
    
    let person = Person(name: "Joanna", age: 21)
    
    if let ageKeyPath = keyPaths["age"]
    {
      let age = person[keyPath: ageKeyPath]
      
      print(age)
    }

The print(age) results in :

Optional(Optional(21))

… and, if you use "po" in the console, you get :

(lldb) po age
    Optional<Any>
      some : Optional<Int>
        some : 21

What seems a little bizarre is, if age is nil, for print(age)you get :

Optional(nil)

… and for `po age', you get :

(lldb) po age
    Optional<Any>
      some : nil

So, in effect, you can have an optional whose .some case is nil :open_mouth:

(Martin R) #4

Yes, you can have an arbitrary level of optionals:

let a: Int??? = .none
print(a) // nil
let b: Int??? = .some(.none)
print(b) // Optional(nil)
let c: Int??? = .some(.some(.none))
print(c) // Optional(Optional(nil))
let d: Int??? = .some(.some(.some(21)))
print(d) // Optional(Optional(Optional(21)))

Another example is given in the “Nested Optionals” section of Optionals Case Study: valuesForKeys from the Swift Blog.

In your case, person[keyPath: ageKeyPath] is

  • nil if the key path does not exist for that value,
  • Optional(nil) if the key path exists, but its value is nil, or
  • Optional(Optional(someInt)) if the key path exists and its value is not nil

If you are only interested in the last case then you could use an if-statement with case-let and a (double) optional pattern:

if let ageKeyPath = keyPaths["age"] {
    if case let age?? = person[keyPath: ageKeyPath] as? Int? {
        print(age) // 21
    }
}

Or – if the dictionary contains only key paths for different members of the same (Person) type – use a PartialKeyPath instead of AnyKeyPath:

let keyPaths: [String : PartialKeyPath<Person>] = ["name" : \Person.name, "age" : \Person.age]
let person = Person(name: "Joanna", age: 21)
if let ageKeyPath = keyPaths["age"] {
    if let age = person[keyPath: ageKeyPath] as? Int {
        print(age) // 21
    }
}
1 Like
(Joanna Carter) #5

Thanks for the ideas; I can see where you're coming from with this but…

With the following real code :

      if case let value?? = self.subject[keyPath: keyPath] as? CustomStringConvertible?
      {
        self.view.text = value.description
      }
      else
      {
        self.view.text = self.isEditing ? "" : "«nil»"
      }

… because I've extended Optional :

extension Optional : CustomStringConvertible
{
  public var description: String
  {
    if case .some(let value) = self
    {
      return String(describing: value)
    }
    
    return "«nil»"
  }
}

… for other uses, where the nested optional doesn't arise.

As it is, the execution path never enters the else clause, which has to decide whether to assign: an empty string if the text field is editing, or «nil» if not, because value is always non-nil :disappointed:

Which is why I am currently using :

    let value = subject[keyPath: keyPath]
    
    if case .some(Optional<Any>.some(let v)) = value
    {
      self.view.text = v.description
    }
    else
    {
      self.view.text = self.isEditing ? "" : "«nil»"
    }

I was just trying to see if I had missed anything obvious that was simpler.

(Svein Halvor Halvorsen) #6

This is because you're using AnyKeyPath. You can use it on all types and extract an Any?, which is .none if the keypath doesn't exist on that type, and .some if it does. Since the property you're extracting also happens to be an optional, you're getting nested optionals. This works as intended, and I can't see any other correct way of dealing with this.

Is there any reason you can't use PartialKeyPath instead?

(Joanna Carter) #7

Thanks for the confirmation of how things are. As I mentioned, I was just trying to find a "neater" way of determining if the eventual value was nil or not.

Yes. It would mean adding an associated type to this protocol :

public protocol KeyPathDeclaration
{
  static var keyPaths: [String : AnyKeyPath] { get }
}


extension KeyPathDeclaration
{
  public static func keyPathForProperty(name: String) -> AnyKeyPath?
  {
    return keyPaths[name]
  }
}

… to make :

public protocol KeyPathDeclaration
{
  associatedtype RootType

  static var keyPaths: [String : PartialKeyPath<RootType>] { get }
}


extension KeyPathDeclaration
{
  public static func keyPathForProperty(name: String) -> PartialKeyPath<RootType>?
  {
    return keyPaths[name]
  }
}

… which the causes the compiler to barf on code like this :

  public weak var subject: (AnyObject & KeyPathValueSetter & KeyPathDeclaration)!

Even if I used Self instead of the associated type, I would end up in the same position. So, until we get existentials sorted, I really don't want to go to all the trouble of having to type-erase something else :unamused:

#8

Have you seen this thread about flattening nested optionals?

Without knowing the rest of use-case, on its face this description sounds like an exceptionally-complicated design. Have you considered simplifying your architecture so you don’t need to do this sort of thing at all?

(Joanna Carter) #9

Absolutely love the Flattenable extension to Optional; so much neater to write and read :yum:

The problem is that this is for a framework that has to work with both optional and non-optional properties of a type. If you want to know more, you need to come to iOSDevUK in September, when I shall be presenting the end result :sunglasses: