Why is optional-chaining allowed here?

Ran into something curious (to me) today. This compiles as expected:

let opts = ["abc", "de", nil]

let a = opts
    .compactMap { $0 }
    .filter { $0.count > 2 }

// OK; `a` is a `[String]`

But this also compiles, even though the parameter into filter’s closure is non-optional: (EDIT: oops, I was mistaken here! $0 in the filter is a String?.)

let b = opts
    .compactMap { $0 }
    .filter { $0?.count ?? 0 > 2 }

// OK (!); `b` is a `[String?]`

Binding the result of the compactMap and then applying the filter to that, though, won’t compile:

let cPart1 = opts
    .compactMap { $0 }

// `cPart1` is a `[String]`

let cPart2 = cPart1
    .filter { $0?.count ?? 0 > 2 }

// err: Cannot use optional chaining on non-optional value of type 'String'

Is b a compiler bug? An expected limitation of the type-checker? An intended feature?

Thanks!

1 Like

I believe this is [SR-16075] `map(foo)` and `map{ foo($0) } ` return different results · Issue #58336 · swiftlang/swift · GitHub; i.e., in typechecking .filter, the compiler sees an optional argument, and the only way that would be possible is if the result of compactMap returns [String?] — so $0 in your compactMap is implicitly promoted to String??.

(See also swift - Why do `map(foo)` and `map{ foo($0) }` return different results? - Stack Overflow)

You can also see the difference between having a single-line and a multi-line closure affect type inference:

let d = opts
    .compactMap { $0 }
    .filter { $0?.count ?? 0 > 2 }
    
print(type(of: d)) // => Array<Optional<String>>
print(d) // => [Optional("abc")]

let e = opts
    .compactMap { _ = $0; return $0 }
    .filter { $0?.count ?? 0 > 2 }
    
print(type(of: e)) // => Array<String>
print(e) // ["abc"]
5 Likes

Thank you, this was really interesting!

1 Like

Always make sure to call compactMap twice! :v::neutral_face:

opts
  .compactMap { $0 }
  .compactMap { $0 as String }
  .filter { $0?.count ?? 0 > 2 } // Cannot use optional chaining on non-optional value of type 'String'
opts
  .compactMap(\.self)
  .compactMap(\String.self)
  .filter { $0?.count ?? 0 > 2 } // Cannot use optional chaining on non-optional value of type 'String'
opts
  .compactMap(\String.self)
  .filter { $0?.count ?? 0 > 2 } // Compiles
opts
  .compactMap(\.self)
  .compactMap(\.self)
  .filter { $0?.count ?? 0 > 2 } // Compiles

For real though, compacted() is the way to go, instead of (\.self) / { $0 }.

opts
  .compacted()
  .filter { $0?.count ?? 0 > 2 } // Cannot use optional chaining on non-optional value of type 'String'

I don't think there's a similar safeguard for anything more complex than nil removal. Maybe this ought to be fixed when typed errors are added.

public extension Sequence {
  @inlinable func compactMap<ElementOfResult, Error>(
    _: ElementOfResult.Type = ElementOfResult.self,
    _ transform: (Element) throws(Error) -> ElementOfResult?
  ) throws(Error) -> [ElementOfResult] {
    do { return try compactMap(transform) }
    catch { throw error as! Error }
  }
}
opts
  .compactMap(String.self) { $0 }
  .filter { $0?.count ?? 0 > 2 } // Cannot use optional chaining on non-optional value of type 'String'