How Flatmap API Contract Transforms Optional Input to Non Optional Result

Cannot convert return expression in flatMap with a meaningless expression inside

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.

Using Java 8's Optional with Stream::flatMap

Java 9

Optional.stream has been added to JDK 9. This enables you to do the following, without the need of any helper method:

Optional<Other> result =
things.stream()
.map(this::resolve)
.flatMap(Optional::stream)
.findFirst();

Java 8

Yes, this was a small hole in the API, in that it's somewhat inconvenient to turn an Optional<T> into a zero-or-one length Stream<T>. You could do this:

Optional<Other> result =
things.stream()
.map(this::resolve)
.flatMap(o -> o.isPresent() ? Stream.of(o.get()) : Stream.empty())
.findFirst();

Having the ternary operator inside the flatMap is a bit cumbersome, though, so it might be better to write a little helper function to do this:

/**
* Turns an Optional<T> into a Stream<T> of length zero or one depending upon
* whether a value is present.
*/
static <T> Stream<T> streamopt(Optional<T> opt) {
if (opt.isPresent())
return Stream.of(opt.get());
else
return Stream.empty();
}

Optional<Other> result =
things.stream()
.flatMap(t -> streamopt(resolve(t)))
.findFirst();

Here, I've inlined the call to resolve() instead of having a separate map() operation, but this is a matter of taste.

Java's flatMap on list of list of optional integers

It should be:

List<Integer> flattened = 
list
.stream()
.filter (Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList());

Your flatMap expects a function that transforms a Stream element to a Stream. You should use map instead (to extract the value of the Optional). In addition, you need to filter out empty Optionals (unless you wish to transform them to nulls).

Without the filtering:

List<Integer> flattened = 
list
.stream()
.map(o -> o.orElse(null))
.collect(Collectors.toList());

Combine weird compile error after placing a print statement in a flatMap operator closure

Multi-statement closures do not take part in type inference, so making the closure to flatMap a multi-statement one, you are somehow causing it to incorrectly infer the type parameters of scan. You can provide the types it needs by writing them out in the closure:

.flatMap { storyIDs -> AnyPublisher<Story, API.Error> in
print("StoryIDs are \(storyIDs)")
return self.mergedStories(ids: storyIDs)
}

If you just want to print the values you receive, you can call .print() as well.

Update:

I played around with this a bit more, and I found that if you put everything before scan into a let constant, and call scan on that constant, the error moves to somewhere else:

let pub = URLSession.shared
.dataTaskPublisher(for: EndPoint.stories.url)
.map(\.data)
.decode(type: [Int].self, decoder: decoder) //<--- Error fires here now!
.mapError { error -> API.Error in
switch error {
case is URLError:
return Error.addressUnreachable(EndPoint.stories.url)
default:
return Error.invalidResponse
}
}
.filter { !$0.isEmpty }
.flatMap { storyIDs in
print("StoryIDs are \(storyIDs)")
return self.mergedStories(ids: storyIDs)
}

return pub.scan([]) { stories, story -> [Story] in
stories + [story]
}
.map { $0.sorted() }
.eraseToAnyPublisher()

Instance method 'decode(type:decoder:)' requires the types 'URLSession.DataTaskPublisher.Output' (aka '(data: Data, response: URLResponse)') and 'JSONDecoder.Input' (aka 'Data') be equivalent

This time, it caused decode's type arguments to be inferred wrongly. The original error at scan disappeared because now the compiler knows that pub has a definite type, though it had (incorrectly) found an error somewhere else before it could determine the type of pub.

Following this pattern, I made another temporary let constant:

let pub1 = URLSession.shared
.dataTaskPublisher(for: EndPoint.stories.url)
.map(\.data)
let pub2 = pub1
.decode(type: [Int].self, decoder: decoder)
.mapError { error -> API.Error in
switch error {
case is URLError:
return Error.addressUnreachable(EndPoint.stories.url)
default:
return Error.invalidResponse
}
}
.filter { !$0.isEmpty }
.flatMap { storyIDs in
print("StoryIDs are \(storyIDs)")
return self.mergedStories(ids: storyIDs)
}

return pub2.scan([]) { stories, story -> [Story] in
stories + [story]
}
.map { $0.sorted() }
.eraseToAnyPublisher()

Finally, the compiler shows a useful message at storyIDs in, which leads use to the solution at the start of the answer:

Unable to infer complex closure return type; add explicit type to disambiguate

It even tells us what type we should insert!

Swift reduce - why is value optional?

In order for you to get this result at all (as far as I can tell), the enclosing function must return an Int?. The implication is that reduce can return an optional. Absent the conditional, the compiler can determine that reduce will never return nil, i.e., total is never nil. So, the compiler infers that the return type of the closure is Int. The compiler appears to be entangling type inferencing for the reduce closure and total. Once you add the conditional, the compiler is incapable of determining whether the reduce will return nil or not. Now when it unnecessarily infers the type for total it gets it wrong.

To me this looks like a case of Swift type inferencing gone astray. Clearly, total is never nil based on the documentation of enumerated.

If you modify the code slightly you get the expected result:

   return first.enumerated().reduce(0) { (total: Int, letter) in
let index = first.index(first.startIndex, offsetBy: letter.offset)
if first[index] != second[index]{
return total + 1
}
return total
}

Swift makes a lot of type inferences and it is really great because I get strong typing while retaining many of the benefits of a dynamic language. In my experience, however, swift's inferences can be mystifying sometimes. It handles arcane situations with ease and stumbles over something I think is obvious.

It looks like a bug to me.

Swift 4.2 weird -.map- behavior

The way map works in Swift (and elsewhere) is that it can map (change) input from one type to another. So in your case, you will call map on your [CBPeripheral] and for each element, the map function will return another value. So, if the identifier is a String, you will end out with an array of Strings.

That is how it should work.

However :)

In your case, you have defined your mapping function like so

(perif) -> Void

Meaning that you will run through your array and call map, with a function that returns nothing. Meaning that the outcome will be an array filled with Void values.

Therefore, as others are suggesting in the comments, if you just want to examine the content of your cbPerifList, you are better of using a for in loop like so

for cbPerif in cbPerifList {
print(cbPerif.identifier)
}

If however, you're interested in getting an array of the names of your CBPeripherals you can do something like this

let names = cbPerifList.map { $0.identifier }

A comment to the above: To read more about how we can end up just writing { $0.name } have a look at the chapter about Closures in the Swift documentation, or this answer

Hope that helps you.



Related Topics



Leave a reply



Submit