Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

"Cannot convert return expression" in flatMap with a meaningless expression inside

I was examining .lazy for high order functions and have got some interesting compile errors related to flatMap function (and possibly others)

Examples

 
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)
}

Commenting out a bit

 
array
    .flatMap {
//        print("DD")
        return $0
    }
    .forEach {
        print("SS")
        print($0)
}

And everything works.. even more interesting example

 
array
    .flatMap {
        let z = $0
        return $0  // Or return z - all is the same "Cannot convert return expression of type 'Int' to return type 'String?'"
    }
    .forEach {
        print("SS")
        print($0)
}

What could cause that behavior?

like image 396
Pavel Stepanov Avatar asked Dec 25 '17 11:12

Pavel Stepanov


2 Answers

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.

like image 125
Hamish Avatar answered Nov 12 '22 16:11

Hamish


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

enter image description here


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]
    
like image 39
vadian Avatar answered Nov 12 '22 16:11

vadian