-
-
Notifications
You must be signed in to change notification settings - Fork 28
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
composeEither
: fixed-scope flatten
without transcations
#123
Comments
flatten
without transcationscomposeEither
: fixed-scope flatten
without transcations
I encountered a case in the past that also required a combo of val inputsSignal: Signal[List[I]] = ???
def veryExpensiveFn(input: I): O = ???
val outputsSignal: Signal[List[O]] = inputsSignal
.split(???) {(_, _, inputSignal) => inputSignal.map(veryExpensiveFn)}
.flatMap { (outputSignals: List[Signal[O]]) => new CombineSignalN[???](???) } Like you said, the code above is practically impossible without IMO, this will pop up more especially if we introduce #103, so a long term support is very welcomed. |
@HollandDM I'm afraid that the approach I outlined is not scalable to the more general A signal of In contrast, a signal of Airstream uses It would be useful to support dynamic |
After some reading, I think Angular's push & pull approach can be modified to be suitable for our system. Maybe one |
Hm. I could be wrong, but I don't think that Angular's approach requires It's easy to see how their algorithm solves the basic diamond glitch case, so let's skip that. I wonder if this kind of algorithm is able to handle the flatMap operator... The Then, for each signal that the So... suppose Now, how does the pull stage decide which observables will be pulled first, and more importantly – does it matter? At first glance, it seems that order shouldn't matter. When a signal's value is being pulled, it knows which other signals it depends on, it can pull their values, and so on, recursively. After that is done, nothing else should be able to update those values we pulled... right? Well, it seems that way, at least. There are several questions remaining:
It does seem that this kind of system requires more overhead. Not only does it need two passes, but the passes need to go deeper (e.g. past any I'm not in a rush to look more into this, but it's an interesting avenue to explore eventually. Although, I'm not sure how applicable the findings will be, since Angular-style signals are intended for a very different, callback-driven usage style, and they don't seem to have a concept of event streams. |
Background
Flattening Airstream observables (using
flatMap
orflatten
or any equivalent means) necessarily results in an observable that establishes a transaction boundary, i.e. it always emits events in a new transaction. Emitting events in new transactions is generally undesirable because it may cause FRP glitches, but, in short, in practice this actually has no practical harm if you only useflatMap
/flatten
when necessary, i.e. when no other operators (such ascombineWith
) would suffice – review the docs if any of this is not clear.However, sometimes, (hopefully rarely, but still) we need
flatMap
not because the required behaviour is conceptually impossible to express without a general-purposeflatMap
method, but because it is merely practically impossible, i.e. because Airstream lacks a specialized operator (that could possibly exist in principle) that would perform the task without creating a new transaction.This issue identifies a use case that currently requires flattening, and proposes a specialized operator that would perform the same task without creating a new transaction, enabling users to express branched computations without fearing FRP glitches.
Use case:
splitEither
+flatten
Currently (in the latest 17.x release) we can do this (optional types ascribed for clarity):
Remember, the new
splitEither
operator has the same semantics as the regularsplit
operator, except instead of operating on each item in a collection, it operates on the left branch and right branches ofEither
. Basically we treat each incomingEither
as a list of exactly one item, with.isRight
as thekey
of that item. Hopefully that makes sense.So, what the code above achieves, is it splits the processing of Either's left and right branches, and then merges them back together. It works, but the problem is that we need this
flatten
at the end. Thatflatten
is a general purpose operator, and it fires all events in a new transaction.However, I believe that this
flatten
here is not necessary, conceptually, because if you look closely at this use case, you can see that we only need to mirror a fixed, static set of signals (makeLeftSignal(leftSignal)
andmakeRightSignal(rightSignal)
). But that is significantly less powerful than whatflatten
can do. In fact, it is precisely the fact thatflatten
can mirror an arbitrary set of signals, that is not fully known at signal creation time, that requires it to emit events in a new transaction, so perhaps we can drop that requirement with a careful implementation.MergeStream equivalence
A
signal.flatten
operator that only mirrors a fixed set of input signals would be somewhat equivalent toMergeStream
, so it would be...MergeSignal
I guess. We don't have a general-purposeMergeSignal
, because it's not clear how to merge the signals' initial values, but in case ofsplitEither().flatten
, the initial values of the left and right branches are actually mutually exclusive, meaning that, if the parent signal emits aLeft
, the "flattened" output only needs to mirror the value of the left signal, ignoring the value of the right signal, and vice-versa. That works out very well for us to create a sort of a narrowly specializedMergeSignal
for this specific use case.Proposal:
composeEither
So, instead of using a combination of
splitEither
+flatten
, I propose to implement a dedicatedcomposeEither
operator that would work just likesplitEither
, except it would include the.flatten
functionality internally, and avoid the need to fire events in a new transaction.I was hoping to sneak this into 17.x release, thinking that it wouldn't be too hard, but actually even though the idea is simple in principle, the implementation needs to bypass a lot of Airstream's protections in order to work, and that means a lot of ugly code, careful analysis, testing, etc. Bottom line, I still think it should be possible, but it's much more work than I can afford to expend right now. And so, here's a brain dump ticket instead.
Long term
I think once we have this
composeEither
functionality, other similar use cases might pop up. Certainly we need the same forOption
,Status
, etc. – all the data types that have a fixed number of mutually exclusive branches should probably use the same implementation.Perhaps the new functionality and the new understanding obtained from implementing this will help us find more opportunities to further narrow the gap between "conceptually impossible" and "practically impossible" when it comes to avoiding the use of
flatMap
/flatten
.Current status
Note to self – I sketched out some notes in the compose-either branch. Not much code, just some comments for the most part.
The text was updated successfully, but these errors were encountered: