Closed romansl closed 8 years ago
You can still do this fine without "many firings per transaction". Let's say you have a block of changes to a UI that come in from a server. If they affect different widgets, then that's no problem - just send them all to the different widgets within a single transaction. They'll be simultaneous, but that is no problem. You'll get the flicker-free UI update you wanted.
If several of them affect one widget - let's say 10 items are being added to a list - then you can also send them within the same transaction. You just need to represent each one as a list of one item, then pass a 'concatenate' function to the StreamSink constructor.
Disallowing multiple firings per transaction doesn't prevent you representing multiple changes in a single transaction. It just means that you have to represent them as a list. What it's doing is requiring you to make your intentions a little more explicit.
The code becomes much more complicated in this case. Compare it.
Many per tx:
val sink1 = Sodium.streamSink<Message1>()
val sink2 = Sodium.streamSink<Message2>()
val filterMap: Stream<Message2> = sink1
.filter { ... }
.map { Message2(...) }
val mergeResult: Stream<Message2> = sink2.merge(filterMap)
val cell2 = mergeResult.hold(null)
Sodium.tx {
messages.forEach { message ->
when (message) {
is Message1 -> sink1.send(message)
is Message2 -> sink2.send(message)
}
}
}
One per tx:
val sink1 = Sodium.streamSink<List<Message1>>()
val sink2 = Sodium.streamSink<List<Message2>>()
val filterMap: Stream<List<Message2>> = sink1
.map {
it.filter { ... }.map { Message2(...) }
}
val mergeResult: Stream<List<Message2>> = sink2.merge(filterMap) { a1, a2 ->
a1 + a2
}
val cell2 = mergeResult
.filter { it.isNotEmpty() }
.map { it.last() }
.hold(null)
Sodium.tx {
val messages1 = ArrayList<Message1>()
val messages2 = ArrayList<Message2>()
messages.forEach { message ->
when (message) {
is Message1 -> messages1.add(message)
is Message2 -> messages2.add(message)
}
}
if (messages1.isNotEmpty()) {
sink1.send(messages1)
}
if (messages2.isNotEmpty()) {
sink2.send(messages2)
}
}
The main motivation of rejection "many per tx" mode was to lower complexity. I think we got the opposite effect.
I consider your issue just a shortcoming of the ArrayList
's API. Your problem goes away by simply creating a (static
) helper function which constructs an ArrayList
of one item from its only argument. Ideally that'd be a constructor of ArrayList
of course…
I do not understand. Can you rewrite my first example simpler?
Sorry, misread your example in a hurry. Indeed, I think your second example makes it clearer what's going on there.
I don't know about Java but in Haskell you can define functions conditionally such that they only exist for, say, Stream<T>
where T
has a certain operation (e.g. append) defined. Then, sink1.map{...}.merge(sink2)
would exist given that the elements (arrays) can be pairwise combined in a default way.
Roman,
The design goal is to make Sodium as semantically simple as possible, and one-per-TX gives us something very valuable: Total non-detectability of event firing order means event simultaneity holds everywhere, and that gives you fewer details to have to think about it.
In your example, the code is longer. This is largely because Java/Kotlin - not being functional languages - do not have proper functional lists built in.
In Haskell it would look something like this:
(s1, send1) <- sync $ newStream (<>)
sync $ do
send1 [message1]
send1 [message2]
The long term solution to this is to use some library that gives you functional data structures. Historical factors to do with the languages we are using are not a good justification to compromise the semantics. The languages will eventually catch up.
Steve
I'm not going to fix anything here.
In my practice I found interesting application of this idea. Suppose we have an MVC architecture. Model is constructed using Sodium. Model may produce very big amount of events, for example by receiving data from server. If our commands run in "one command per tx" mode, we have very big amount of listener reactions in our Controller and View. Each reaction may produce UI changes, taking a lot of time to render and annoing flickering. If we use "many firings per tx" mode, we can reduce amount of commands by apply
coalesce
andmerge
operations. Also many listeners is bound to the Cells. Cells receive only the last event from stream per transaction.Of course we can allpy RXJava-like
debounce
operation, but delay is not solution for all situations.What do you think?