“Cannot convert return expression” in flatMap with a meaningless expression inside

旧时模样 提交于 2019-12-01 05:06:50

The flatMap(_:) method on Sequence currently (as of Swift 4) has two different meanings:

  • It can take a transform closure that returns an optional T?, and it will return a [T], filtering out the nil results (this overload is to be renamed to compactMap(_:) in a future version).

    public func flatMap<ElementOfResult>(
      _ transform: (Element) throws -> ElementOfResult?
    ) rethrows -> [ElementOfResult]
  • It can take a transform closure that returns a Sequence, and it will return an array containing the concatenation of all the resulting sequences.

    public func flatMap<SegmentOfResult : Sequence>(
      _ transform: (Element) throws -> SegmentOfResult
    ) rethrows -> [SegmentOfResult.Element]

Now, in Swift 4, String became a RangeReplaceableCollection (and therefore a Sequence). So Swift 3 code that did this:

// returns ["foo"], as using the `nil` filtering flatMap, the elements in the closure
// are implicitly promoted to optional strings.
["foo"].flatMap { $0 }

now does this:

// returns ["f", "o", "o"], a [Character], as using the Sequence concatenation flatMap,
// as String is now a Sequence (compiler favours this overload as it avoids the implicit
// conversion from String to String?)
["foo"].flatMap { $0 } 

To preserve source compatibility, specialised flatMap overloads were added for strings:

//===----------------------------------------------------------------------===//
// The following overloads of flatMap are carefully crafted to allow the code
// like the following:
//   ["hello"].flatMap { $0 }
// return an array of strings without any type context in Swift 3 mode, at the
// same time allowing the following code snippet to compile:
//   [0, 1].flatMap { x in
//     if String(x) == "foo" { return "bar" } else { return nil }
//   }
// Note that the second overload is declared on a more specific protocol.
// See: test/stdlib/StringFlatMap.swift for tests.
extension Sequence {
  @_inlineable // FIXME(sil-serialize-all)
  @available(swift, obsoleted: 4)
  public func flatMap(
    _ transform: (Element) throws -> String
  ) rethrows -> [String] {
    return try map(transform)
  }
}

extension Collection {
  @_inlineable // FIXME(sil-serialize-all)
  public func flatMap(
    _ transform: (Element) throws -> String?
  ) rethrows -> [String] {
    return try _flatMap(transform)
  }
}

Such that the above usage would still return a [String] in Swift 3 compatibility mode, but a [Character] in Swift 4.

So, why does

let array = [1, 2, 3, 4, 5, 6]

array
    .flatMap {
        print("DD")
        return $0 // Cannot convert return expression of type 'Int' to return type 'String?'
    }
    .forEach {
        print("SS")
        print($0)
    }

tell you that the closure should return a String??

Well, Swift currently doesn't infer parameter and return types for multi-statement closures (see this Q&A for more info). So the flatMap(_:) overloads where the closure returns either a generic T? or a generic S : Sequence aren't eligible to be called without explicit type annotations, as they would require type inference to satisfy the generic placeholders.

Thus, the only overload that is eligible, is the special String source compatibility one, so the compiler is expecting the closure to return a String?.

To fix this, you can explicitly annotate the return type of the closure:

array
  .flatMap { i -> Int? in
    print("DD")
    return i
  }
  .forEach {
    print("SS")
    print($0)
  }

But if you're not actually using the optional filtering functionality of this flatMap(_:) overload in your real code, you should use map(_:) instead.

flatMap can have different meanings depending on the context. You might tell the compiler more precisely which one you are intending to use.


The two usual purposes to apply flatMap to an array which the compiler can infer is to

  • flatten nested arrays

    let array = [[1, 2, 3, 4, 5, 6], [7, 8, 9]]
    let flattened = array.flatMap{$0}
    print(flattened) // [1, 2, 3, 4, 5, 6, 7, 8, 9]
    
  • map to another type and filter optionals

    let array = ["1", "a", "2", "3", "b", "4", "5", "6"]
    let flattened = array.flatMap{ Int($0) }
    print(flattened) // [1, 2, 3, 4, 5, 6]
    
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!