SodiumFRP / sodium

Sodium - Functional Reactive Programming (FRP) Library for multiple languages
http://sodium.nz/
Other
848 stars 138 forks source link

Back to "many firings per tx" idea. #83

Closed romansl closed 8 years ago

romansl commented 8 years ago

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 and merge 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?

the-real-blackh commented 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.

romansl commented 8 years ago

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.

pyrtsa commented 8 years ago

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…

romansl commented 8 years ago

I do not understand. Can you rewrite my first example simpler?

pyrtsa commented 8 years ago

Sorry, misread your example in a hurry. Indeed, I think your second example makes it clearer what's going on there.

pyrtsa commented 8 years ago

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.

the-real-blackh commented 8 years ago

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

the-real-blackh commented 8 years ago

I'm not going to fix anything here.