-
Notifications
You must be signed in to change notification settings - Fork 70
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement keyed side effects in place of / as underpinnings to Worker (Grand Unified Workflow Theory) #1021
Comments
Not sure if this issue is open for discussion. I really like the proposal, it's a lot clearer and easier to understand. The unclear part to me is how you'd deal with states: // Proposed:
override fun render(
props: RequestParameters,
state: State,
context: RenderContext<State, Output>
) {
when (state) {
is Something -> context.onFirstRender(..) { .. }
}
} While |
It's a lousy name, suggestions welcome. It basically means "I am only going to use this lambda the first time you call render with this key" -- calling it again in later renderings with the same key will keep it running, just like today's workers. |
|
My concern with |
I like to think of this change as replacing a type with a function. The type was sibling to Workflow, and copied all its semantic questions. The function is much more constrained and with a much smaller API surface, and had much fewer questions. |
The arguments to this function are the key that defines how long the task is kept alive, and distinguishes different calls. One thing we didn't talk about yesterday that I would like to propose is making the parameter a vararg, so it's easy to pass in multiple values to use as the key. This addresses the concert @bencochran raised about often only wanted to use a subset of fields for equivalence in a way that doesn't require creating a wrapper explicitly. It also mirrors the Compose |
How does the |
Ben's solution passes a ReactiveSwift fun runSideEffect(key: Any, sideEffect: suspend () -> Unit) The |
Sorry, haven’t fully caught up here, but one quick note:
That was mostly for the prototype. I’d imagine we’d either do something like the |
Oops, I think I meant to type Lifetime - fixed my comment. |
We had a long, deep discussion on this last week, and settled on basically the original proposal:
Alternatives we played with and rejected include:
Let's drill into that last point a bit more. It's not something we'd really realized before, and it's pretty illuminating. Anything that looks like a Workflow lifecycle is really almost entirely about side effects (today's Workers). In the new model, not terribly different than the current one, the Workflow tree effectively has a keyed map of running side effects that are:
The keys to this side effect table are namespaced by the workflow tree. What looks like a child workflow "starting" when it is first rendered is almost entirely down to the workers that it declares when it first renders; and which are ended when it is no longer rendered, and so no longer declares them. The Workflow / SideEffect dichotomy allows Workflow implementations to stay purely declarative and functional, and makes it clear that "real work" belongs in the SideEffect functions they define. Today's Workers and UI event ActionSinks can be built from the new SideEffect primitive. (Or maybe it's that a single sink per workflow is part of the SideEffect plumbing? I lost track there.) |
While working on prepping Ben’s iOS GUWT implementation for review, I kept bumping into a few things that I was trying to wrap my head around. I took some time to crystalize my concerns from the first-principles perspective. I wanted to note the concerns here and get the team’s feedback before proceeding with the implementation. Happy to disagree and commit :) It boils down to the following issues: TypeGUWT proposes changing the awaitResult(for: Worker) To: runSideEffect(key: String, action: (Lifetime) -> Void) ConcernWhile it looks like we’re deleting the
Switching to a non-nominal type would limit how flexible the API is. For example, to create a new kind of convenient While we can work around this, I’m not entirely sure if there’s a win by switching from a nominal type to a non-nominal type. KeyGUWT proposes replacing ConcernThis adds additional responsibility on the caller to make sure the In contrast, Why?I spent some time digging into why we need the Wins - DependenciesI'm excited about removing the ProposalFrom the Swift perspective, I would introduce public protocol SideEffect {
associatedtype Output
func run(sink: Sink<Output>) -> Lifetime
func isEquivalent(to otherSideEffect: Self) -> Bool
} The two big differences being:
|
The proposal as I understood it wasn’t to replace |
👍 Yes, when we had first discussed this, I was pretty excited about this idea. However, digging into this now, it feels like all we've done is to "require" the For example, by switching to using a nominal type for Using nominal types, we can get an API very similar to our current proposal like in here and we'll retain the ability to introduce a |
It seems to me that sink is a big hand wave in this proposal, and reopens a lot of the conversations that were finally resolved by the GUWT proposal. Is there one sink per workflow? Per side effect? Is it a buffer? If it's the same sink we're already using for UI events, that's a drastic change to Worker semantics, and one we've rejected at least twice already: today each worker is effectively its own sink. If workers start sharing the same buffered sink used for UI events, things get dramatically less deterministic than they are today. Most of the rest of the argument is about the side effects being harder to work with than workers, which was explicitly not the proposal. We consistently discussed side effects as a fundamental low level building block, upon which we would build conveniences like the Worker abstraction — one which would likely never be used directly by feature developers. So, you're working to solve a problem that we don't have, and introducing unnecessary complexity to do so.
This feels to me like a rehash of settled issues. |
|
The current GUWT proposal uses the UI sink for events, like here. Current GUWT proposal: let sink = context.makeSink(Action.self)
context.runSideEffect(key: "") {
sink.send(.action)
} My proposal creates the sink from within the infra and passes it to
Got it. Since this is part of the public API, I was trying to think through all the different ways it can be used/misused.
I was positing that we're doing exactly that by switching to non-nominal types and adding a hashable requirement. Overall, I think the proposal is a step forward. I wanted to make sure the concerns and it's justifications are documented. Since the team feels the proposal as it stands is good, I'll continue with the implementation as-designed. |
…end functions. - `Sink.sendAndAwaitApplication()` - `Flow.collectToSink()` These helpers will be used to implement Workers using side effects (#12). GUWT workers are described in square/workflow#1021.
First part of Kotlin implementation of square/workflow#1021. Kotlin counterpart to square/workflow#1174. This implementation intentionally does not run the side effect coroutine on the `workerContext` `CoroutineContext` that is threaded through the runtime for testing infrastructure. Initially, workers ran in the same context as the workflow runtime. The behavior of running workers on a different dispatcher by default (`Unconfined`) was introduced in square/workflow#851 as an optimization to reduce the overhead for running workers that only perform wiring tasks with other async libraries. This was a theoretical optimization, since running on the `Unconfined` dispatcher inherently involves less dispatching work, but the overhead of dispatching wiring coroutines was never actually shown to be a problem. Additionally, because tests often need to be in full control of dispatchers, the ability to override the context used for workers was introduced in square/workflow#940, which introduced `workerContext`. I am dropping that complexity here because it adds a decent amount of complexity to worker/side effect machinery without any proven value. It is also complexity that needs to be documented, and is probably just more confusing than anything. The old behavior for workers is maintained for now to reduce the risk of this change, but side effects will always run in the workflow runtime's context. This is nice and simple and unsurprising and easy to reason about.
This is the Kotlin half of square/workflow#1021.
This is the Kotlin half of square/workflow#1021.
This is the Kotlin half of square/workflow#1021.
The side effect implementations are done on both platforms. Closing. |
This is the Kotlin half of square/workflow#1021.
This is the Kotlin half of square/workflow#1021.
This is the Kotlin half of square/workflow#1021.
This is the Kotlin half of square/workflow#1021.
This fixes #82, which is the Kotlin half of square/workflow#1021.
This fixes #82, which is the Kotlin half of square/workflow#1021.
This fixes #82, which is the Kotlin half of square/workflow#1021. Also fixes square/workflow#1197.
This fixes #82, which is the Kotlin half of square/workflow#1021. Also fixes square/workflow#1197.
This fixes #82, which is the Kotlin half of square/workflow#1021. Also fixes square/workflow#1197.
Issues: - Fixes #82, which is the Kotlin half of square/workflow#1021. - Fixes #92. - Fixes square/workflow#1197.
Issues: - Fixes #82, which is the Kotlin half of square/workflow#1021. - Fixes #92. - Fixes square/workflow#1197.
Issues: - Fixes #82, which is the Kotlin half of square/workflow#1021. - Fixes #92. - Fixes square/workflow#1197.
Issues: - Fixes #82, which is the Kotlin half of square/workflow#1021. - Fixes #92. - Fixes square/workflow#1197.
Issues: - Fixes #82, which is the Kotlin half of square/workflow#1021. - Fixes #92. - Fixes square/workflow#1197.
Issues: - Fixes #82, which is the Kotlin half of square/workflow#1021. - Fixes #92. - Fixes square/workflow#1197.
Issues: - Fixes #82, which is the Kotlin half of square/workflow#1021. - Fixes #92. - Fixes square/workflow#1197.
Issues: - Fixes #82, which is the Kotlin half of square/workflow#1021. - Fixes #92. - Fixes square/workflow#1197.
Issues: - Fixes #82, which is the Kotlin half of square/workflow#1021. - Fixes #92. - Fixes square/workflow#1197.
Issues: - Fixes #82, which is the Kotlin half of square/workflow#1021. - Fixes #92. - Fixes square/workflow#1197.
Issues: - Fixes #82, which is the Kotlin half of square/workflow#1021. - Fixes #92. - Fixes square/workflow#1197.
Issues: - Fixes #82, which is the Kotlin half of square/workflow#1021. - Fixes #92. - Fixes square/workflow#1197.
There are lot of nagging questions and bugs around Workers, Workflows and their consistency between each other and the two platforms.
SignalProducer.asWorker
? (Add SignalProducer.asWorker to Swift #581)Context.subscribe
, or drop that function entirely? ([iOS] If 2 signals fire in the same run loop, the second is ignored #744)After a marathon discussion this afternoon, @zach-klippenstein @bencochran @dhavalshreyas and I are pretty convinced that most of these issues can be resolved, or at least become a lot simpler to address, if we drop Worker as a separate concept. We replace it with one new Workflow feature that allows them to do the same kind of work,
RenderContext.onFirstRender
.It should be possible to retrofit existing Workers as convenience implementations built on this call, allowing this to be a transparent change. And this seems like no more work than addressing all of the issues listed above in isolation — probably a lot less when you take into account the related design discussions that never seem to resolve.
I'm not going to try to capture all of the details, just enough that we don't forget the conversation. Counting on @zach-klippenstein and @bencochran to iterate here.
It is not expected that anyone would call that method inline. Rather, it's the primitive we use to build various operators.
Worker could be reproduced something like this:
Worker in Swift:
Could provide a convenience for a long lived RPC request something like this:
The text was updated successfully, but these errors were encountered: