`Equatable` conformance of `Actions` with untyped `Errors`

Hi all,

I have some questions regarding Equatable conformance of Actions in light of the use of async/await and the Effect.task() helper methods. I apologize if this was discussed before, but I couldn't find any pointers.

So far, I got in the habit of making all my Actions conform to Equatable. This has the consequence, that the Errors I use in my Effects also conform to Equatable, because I typically handle them as Results in my Actions. Ocassionally, I have to use APIs that just throw generic Errors and I typically define wrapper-Errors to make them Equatable.

Since now more and more APIs move to async/await with untyped throws, I found that the necessity to use wrapper-types becomes much more widespread. Therefore, I question my habit to conform all Actions to Equatable. From what I found in my research, the only advantage of this is that the TestStore can distinguish the actions.

My Questions:

  • Is this really the only advantage?
  • Is this still relevant or can the usage of customDump in the TestStore-implementation do these comparisons equally good via introspection?

As a side question, I typically use the Effect.task()-helper to wrap the async tasks. Almost always I have to explicitly make sure that I receive the values on the main thread. E.g. like this:

Effect.task {
   ...
}
.receive(on: DispatchQueue.main)
.eraseToEffect()

Isn't that true for almost all users of Effect.task()? I would welcome some addition of syntactic sugar for this.

2 Likes

Hi @andtie,

I've got some answers/comments below:

  • Is this really the only advantage?

Yes, the only reason to make your actions Equatable is if you want to write tests. However, we do agree that maybe this is to strong of a requirement for many situations, and we do have some ideas to weaken it a bit. Stay tuned :)

  • Is this still relevant or can the usage of customDump in the TestStore-implementation do these comparisons equally good via introspection?

We thought about leveraging customDump to provide a kind of pseudo-equality to all types, but unfortunately it cannot be fully depended on. It's possible for a type to provide a bad conformance to one of the custom dump protocols, which would allow unequal things to have equal dumps.

Isn't that true for almost all users of Effect.task() ? I would welcome some addition of syntactic sugar for this.

As of today we do not recommend people using Effect.task directly in a reducer. It is only appropriate as a tool for constructing live dependencies, which we callout in the documentation here:

The reason for this is because using Effect.task throws a wrench in writing tests. You will be forced to add small expectation waits after doing store.send and before doing store.receive in order to force the task to execute its work.

We are hoping that custom executors will improve some of this stuff (executors are to tasks as schedulers are to publishers?), but we are also working on some new tools that will make it possible to use Effect.task in reducers today. Stay tuned for that too :)

3 Likes

Thank you for the clarification!

Regarding the Effect.task, I see why it shouldn't be used directly in the reducer and I am sorry for being ambiguous about it. I typically use it to connect live-dependencies to my environment. E.g.:

struct SomeEnvironment {
    var refresh: () -> Effect<[String], Error>
}

// and then later:
let environment = SomeEnvironment(
    refresh: {
        Effect.task {
            try await someDataManager.refresh()
        }
        .receive(on: DispatchQueue.main)
        .eraseToEffect()
    }
)

But leveraging custom executors for this sounds like a great idea. My solution, applied generally, can lead to unnecessary thread-hopping if the task already executes on the main thread.