Reputation: 825
My request resulting in AnyPublisher
let foo: AnyPublisher<API.Response, API.Error> = APIClient.get(API.Endpoints.Demo)
now I would like to map the value of this publisher to array of string,
message
in API.Response is of type [String : [String]]
, so my try is
foo.map { $0.message.map { $0.key } }
at this point I would expect I would end up with AnyPublisher<[String], API.Error>
, or just any other publisher but parametrized to <[String], API.Error>
,
instead I end up with Publishers.Map<AnyPublisher<API.Response, API.Error>, [String]>
.
func map<T>(_ transform: @escaping (Self.Output) -> T) -> Publishers.Map<Self, T>
The docs above for .map
function are correct, but why is it working like that - why would someone expect the map to keep the previous publisher type but somehow nested inside there. I can't figure it out, any help appreciated :)
Upvotes: 1
Views: 783
Reputation: 385600
If you are used to other ReactiveX-style libraries, or you're used to a language (like Java or Kotlin or Scala) where objects are always heap-allocated, Combine types can be quite a shock.
Because Publishers.Map
is generic over the type of its upstream publisher (in this case, AnyPublisher<API.Response, API.Error>
), it can store its upstream publisher inline, without needing to wrap the upstream in an existential container or otherwise heap-allocate storage for the upstream.
Map
also optimizes consecutive uses of the map
operator. The Combine framework defines a map
operator for all Publisher
s, and Map
inherits that. But Map
also defines a more specialized map
operator which coalesces consecutive map
operators.
Here's an example:
import Combine
let foo: AnyPublisher<[String: [String]], Error> = Empty().eraseToAnyPublisher()
let chained = foo
.map { Array($0.keys) }
.filter { !$0.isEmpty }
.map { $0.joined(separator: ", ") }
.map { "[\($0)]" }
print("chained", type(of: chained))
What's the type of chained
? It is
Map<
Filter<
Map<
AnyPublisher<
Dictionary<String, Array<String>>,
Error
>,
Array<String>
>
>,
String
>
Notice two things here. There are only two Map
s in the type, even though I used the map
operator three times. That's because the final use of map
uses the Map
type's specialized map
operator, which coalesces the two consecutive uses of map
into a single Map
instance.
And because Map
and Filter
are struct
s, they can be store on the stack or directly inline as properties of other data types. So the value in chained
can be stored on the stack.
In general, Map
might need to store its transform
function on the heap, but it doesn't also need to perform a heap allocation to store its upstream. In the example above, since none of arguments passed to map
and filter
close over any outside variables, Swift might be able to optimize out all heap allocations when creating the chained
value.
The use of generic types here also gives the compiler more knowledge about how the chained
value is constructed, so it might be able to perform other optimizations.
So, applying an operator to a publisher usually returns a different type of publisher. If you need to write a function that takes a publisher as an argument, this means you should make the function generic over the type of publisher. For example, every SwiftUI View
has an onReceive
modifier with this signature:
func onReceive<P>(
_ publisher: P,
perform action: @escaping (P.Output) -> Void
) -> some View where P : Publisher, P.Failure == Never
You can pass any publisher to onReceive
, as long as that publisher's Failure
associated type is Never
. So you can pass a Map
or a Filter
or an AnyPublisher
to onReceive
.
Upvotes: 3