-
-
Notifications
You must be signed in to change notification settings - Fork 30
Description
Background
Flattening Airstream observables (using flatMap
or flatten
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 use flatMap
/ flatten
when necessary, i.e. when no other operators (such as combineWith
) 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-purpose flatMap
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):
val signalOfEither: Signal[Either[L, R]] = ???
def makeLeftSignal(signal: Signal[L]): Signal[Left[L2]] = ???
def makeRightSignal(signal: Signal[R]): Signal[Right[R2]] = ???
val metaSignal: Signal[Signal[Either[L2, R2]]] = signalOfEither.splitEither(
(initialLeft: L, leftSignal: Signal[L]) => makeLeftSignal(leftSignal),
(initialRight: R, rightSignal: Signal[R]) => makeRightSignal(rightSignal)
)
val outputSignal: Signal[Either[L2, R2]] = metaSignal.flatten
Remember, the new splitEither
operator has the same semantics as the regular split
operator, except instead of operating on each item in a collection, it operates on the left branch and right branches of Either
. Basically we treat each incoming Either
as a list of exactly one item, with .isRight
as the key
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. That flatten
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)
and makeRightSignal(rightSignal)
). But that is significantly less powerful than what flatten
can do. In fact, it is precisely the fact that flatten
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 to MergeStream
, so it would be... MergeSignal
I guess. We don't have a general-purpose MergeSignal
, because it's not clear how to merge the signals' initial values, but in case of splitEither().flatten
, the initial values of the left and right branches are actually mutually exclusive, meaning that, if the parent signal emits a Left
, 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 specialized MergeSignal
for this specific use case.
Proposal: composeEither
So, instead of using a combination of splitEither
+ flatten
, I propose to implement a dedicated composeEither
operator that would work just like splitEither
, 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 for Option
, 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.