RFC: Deprecate Effect.fireAndForget
and Effect.task
in favor of unified Effect.run
interface
#1520
Replies: 6 comments 21 replies
-
Would this be an opportune moment to introduce other direction action conveniences? |
Beta Was this translation helpful? Give feedback.
-
Throwing out another naming idea to... "return an effect" return .effect { send in await send(.action) }
return .effect { _ in ... }
return .send(.action) // this still looks right to "send an action" |
Beta Was this translation helpful? Give feedback.
-
I'm also in favor of |
Beta Was this translation helpful? Give feedback.
-
I suggested to unify But why stop there? I think there's also a chance of streamlining |
Beta Was this translation helpful? Give feedback.
-
So, I'm getting non-sendable warning for this one since
struct Reducer: ReducerProtocol {
struct State: Equatable {}
enum Action {
case foo
case bar(TaskResult<String>)
}
var body: some ReducerProtocol<State, Action> {
Reduce { state, action in
switch action {
case .foo:
return .run { action in
await action.send(
await .bar(
TaskResult {
try await Task.sleep(for: .seconds(2))
return "result"
}
)
)
}
case let .bar(result):
print(result)
return .none
}
}
}
} It would work if I mark // ...
switch action {
case .foo:
return .run { @MainActor action in
action.send(
await .bar(
TaskResult {
try await Task.sleep(for: .seconds(2))
return "result"
}
)
)
}
Is the 2nd approach a correct practice? (Asking this question as I couldn't find a note about main actor in the description. |
Beta Was this translation helpful? Give feedback.
-
The simplicity of return .task { .delegate(.reorderButtonToggled) }
return .run { await $0(.delegate(.reorderButtonToggled)) } |
Beta Was this translation helpful? Give feedback.
-
The Composable Architecture currently leads folks to three main interfaces for performing effects:
Effect.run
Effect.task
Effect.fireAndForget
After a recent discussion and some thought, we'd like to propose deprecating
task
andfireAndForget
and instead rely on a singlerun
interface, and we would like to outline our reasoning.How we got here
First, some background: when we worked on better support for Swift concurrency, we focussed on distancing the library primitives from Combine, and introducing new primitives for working with structured concurrency. These new primitives were actually largely based off existing primitives that had proven themselves useful, pre-concurrency beta:
Effect.run
feeds zero or more actions back into the store. We had a Combine-friendly version a lot time ago, but finessed the interface when building support forasync
-await
.Effect.fireAndForget
feeds no actions back into the store. It just fires some effectful work off into the void and the store can forget all about it. Prior to the concurrency beta, we had a version that took a simple, synchronous() -> Void
closure, but we beefed this up to take an() async throws -> Void
closure with the beta.Effect.task
can feed a single action back into the store. It was introduced to the library before our more holistic Concurrency beta, and was our first experiment with allowing effects to performasync
-await
work, but we only recommended its use in "live" dependencies because the test store was not equipped for handling this kind of asynchronous work at the time. With the concurrency beta, we beefed it up to behave more like the new and improvedrun
andfireAndForget
helpers.Where we'd like to go
We think it's time to reconsider things. We don't think separate interfaces for
fireAndForget
andtask
are carrying their weight these days, and community members liked @davdroman are coming to the same conclusion. So we'd like to propose deprecating these interfaces, first softly, then loudly on a 1.0 release branch, before fully removing them when 1.0 is out.Effect.fireAndForget
would be more simply handled by anEffect.run
that ignores thesend
argument, and thus cannot feed actions back into the store:It's fewer characters that just as loudly signal intent.
"Fire-and-forget" is also jargon-y language that can cause confusion, not only across language barriers. @alexito4 points out that this separate interface calls its very semantics into question. It can evoke
Task.detached
but does not share any of the same behavior. We think collapsing things to.run { _ in … }
avoids all of these questions and mess.Effect.task
is sugar, but we wonder if this sugar is worth it. Instead we propose that one can simply use run here, as well:While
.task
signals up front that the effect will feed a single action back into the store...we're not sure this is meaningful. It's also not really true. If the effect is cancelled or throws cancellation, it will not feed that single action back into the store.Beyond that,
run
can animate effects, buttask
cannot. To animate atask
you must either upgrade it to arun
, or you must reach for the.animation()
effect modifier. The former shows how fussy a refactoring can be for a change that is simple. The latter is just more surface area bogging the library down.One argument for
.task
is it’s a clean interface for feeding values back into the store:We think such “synchronous” actions are better handled by a non-async interface. While we have
Effect(value: .action)
, it was designed to be paired withEffect.init(error:)
and is part of the Combine APIs we have deprecated. Instead, we propose introducingEffect.send
as a more precise helper:If we had designed effects from scratch with structured concurrency in mind, I'm not sure we would have even come up with these three interfaces. Taking the time to remove two of them should make the library easier to learn and less fussy to work with over time.
So this is the direction we’d like to take things. We understand that it’s more churn on the road to 1.0, but we think it will make the library better and more approachable in the long run. How does that sound to everyone?
Beta Was this translation helpful? Give feedback.
All reactions