Merging effects

Hi I came across the following on combine publishers:

And specifically the part:

private extension TaskGroupsLoader {
    func loadGroups(
        for entries: [Entry]
    ) -> AnyPublisher<[Task.Group], Error> {
        // First, we convert our Entry array into a publisher:
        entries.publisher
                // Then, we use the flatMap operator to convert
                // each Entry element into a nested publisher using
                // the loadGroup method that we implemented earlier:
               .flatMap(loadGroup)
               // Finally, we collect the results from all of our
               // nested publishers into one final array of task groups:
               .collect()
               .eraseToAnyPublisher()
    }
}

But I'm having a hard time matching this with the Effect type.
I'm currently stuck at using .collect() which doesn't work on a collection/array of effects. I've seen zip on Combine Publishers but they don't seem to be made for a collection of effects of variable size. Any tips on how to do this would be greatly appreciated! :)
Here's a simplified version of my code:

enum Action {
    case onAppear
    case responses([APIResponse]) // This can also be of type  Result<[APIResponse], APIError> or [Result<APIResponse, APIError>] depending on what happens in the reducer
}

struct Environment {
     let apiRequest: (APIQuery) -> Effect<APIResponse, APIError>
}

let reducer = Reducer<State, Action, Environment> { state, action, env in 
    switch action {
    case .onAppear:
        let queries: [APIQuery] = returnRandomAmountOfQueries()
        let effects = queries.map(env.apiRequest)
        return effects.collect().map(Action.responses).eraseToEffect()
    case let .responses(responses):
        print(responses)
        return .none
    }
}

To summarize I would like to be able to send a variable amount of effects and group their results, send an action back to the reducer with their collected results once all effects have finished.

Effect is a publisher and will have all the same methods other publishers have, including .collect. I think the part of your code that needs updating is that you invoke .collect on an array of publishers, not on a single publisher. Doing something like this may help:

- let effects = queries.map(env.apiRequest)
+ let effects = queries.publisher.flatMap(env.apiRequest)

Thanks for the pointer, I tried that too and ran into the following errors on the second line of the below code.

let effects = queries.publisher.flatMap(environment.apiClient.getMyBooks)
return effects.collect().map(AppAction.responses).eraseToEffect()

1:
Cannot convert return expression of type 'Effect<Action, Publishers.Collect<Publishers.FlatMap<Effect<APIResponse, APIError>, Publishers.SetFailureType<Publishers.Sequence<[APIQuery], Never>, APIError>>>.Failure>' (aka 'Effect<Action, APIError>') to return type 'Effect<Action, Never>'

2:
Cannot convert value of type '([Result<APIResponse, APIError>]) -> Action' to expected argument type '(Publishers.Collect<Publishers.FlatMap<Effect<APIResponse, APIError>, Publishers.SetFailureType<Publishers.Sequence<[APIQuery], Never>, APIError>>>.Output) -> Action' (aka '(Array<APIResponse>) -> Action')

The error is intense, but it does show what is wrong:

(aka 'Effect<Action, APIError>') to return type 'Effect<Action, Never>'

You are returning an effect that fails with APIError but you need to return one that Never fails. So you either need to ignore the failure, or do .catchToEffect to bundle the output+failure into a Result and pass that along to the action.

2 Likes

Awesome! adding that after .collected() did the trick. I wasn't aware that's what .catchToEffect() was for! So thanks a lot, both learned something and fixed my problem :)