kefirjs / kefir

A Reactive Programming library for JavaScript
https://kefirjs.github.io/kefir/
MIT License
1.87k stars 97 forks source link

[question] Differences in behaviour of `flatten`ed stream #233

Open jeron-diovis opened 7 years ago

jeron-diovis commented 7 years ago

Hi! It found yet another interesting case with .constant method (my favourite one 😄), which I can't explain myself, so I need a help.

A simple snippet:

Kefir.sequentially(100, [ [1, 2, 3] ])
.flatten()
.spy("\ninput")
.scan((a, b) => {
  console.log("[scan] %s + %s", a, b);
  return a + b;
}, 0)
.changes()
.log("output")

Output is pretty predictable:

input <value> 1
[scan] 0 + 1
output <value> 1

input <value> 2
[scan] 1 + 2
output <value> 3

input <value> 3
[scan] 3 + 3
output <value> 6

input <end>
output <end>

Now let's do a little change:

Kefir.constant([1, 2, 3])
.flatten()
.spy("\ninput")
.scan((a, b) => {
  console.log("[scan] %s + %s", a, b);
  return a + b;
}, 0)
.changes()
.log("output")

Output:

input <value> 1
[scan] 0 + 1

input <value> 2
[scan] 1 + 2

input <value> 3
[scan] 3 + 3

input <end>
output <end:current>

It looks quite confusing. Of course, there is a difference – constant creates a property, while sequentially creates a stream. But I can't understand how does it matter in this case. From naive point of view, these two snippets are logically equivalent: they both emit one array value and then end; and in both cases array is then synchronously transformed into series of separate values.

But in case with constant, it all looks like we're still dealing with a single value in the entire chain – note that final sum is not emitted. Even more, if we add .changes() call after .flatten(), then logger only outputs <end> – just like if we've skipped the current value of property. Despite doc says that flatten always creates a stream, and so, if I understand correctly, .changes should do effectively nothing here.

Is it expected behaviour? And if yes, what logic is behind it?

jensklose commented 7 years ago

Yes of course.

The logic is that the differences in behaviour came from changes. You subscribe only the default from a property. First change is the "end".

mAAdhaTTah commented 7 years ago

they both emit one array value and then end

Not exactly; the constant Observable is created with the value already in the stream, whereas the sequentially Observable emits the value into the stream. This is why changes skips the value.

jeron-diovis commented 7 years ago

the constant Observable is created with the value already in the stream

You subscribe only the default from a property. First change is the "end"

I thought than flatten will neutralize the difference then. No matter whether array was emitted or it was initially there – after flatten we always deal with a stream which emits 3 times, and it's value changes 3 times, isn't it? It's 3 separate events, how can they be considered as a single one?

jensklose commented 7 years ago

It isn't.

Kefir.constant([1, 2, 3])
  .flatten()
  .spy()
  .log('observer');

Output:

[constant.flatten] <value> 1
observer <value:current> 1
[constant.flatten] <value> 2
observer <value:current> 2
[constant.flatten] <value> 3
observer <value:current> 3
[constant.flatten] <end>
observer <end:current>

The spy logger don't add currentstate. Nevertheless he is there. So your scan property does what you want, but your changes filters all the current values.

It's a strange example and I should thought about my use of constant. It isn't an event pusher, it is a property holder.

To init a stream you can:

Kefir.later(0, [1, 2, 3])
  .flatten()
jeron-diovis commented 7 years ago

@jensklose this is what I'm asking about. Why it works like this?

I don't think example using constant is strange. Let say I'm writing a test for some stream transformation function (where all transformations are synchronous). I need a stub for it. And then why should I use .later(0, ...) and bring asynchrony in my completely synchronous logic? Instead of just creating a stream with value already there?

It is counter-intuitive as for library user, this what I'm trying to say. constant is something so special that you never can rely on it. For example, .delay() isn't applied to it (at least, this is explicitly noted in docs). Just because. While expected behaviour is https://jsfiddle.net/ryoeghxf/4/.

I want to know, is it a, let say, "side-effect" of library design (and so, ok, we should live with this, at least for now), or all this is intentional (and then what's the real practical purpose) ?

jensklose commented 7 years ago

First of all, it's intentional. And yes for a library user it is important to know the difference of streams and properties.

Your mock should work with constant. But you should avoid the usage of changes because the documentation says:

Converts a property to a stream. If the property has a current value (or error), it will be ignored (subscribers of the stream won't get it).

What's the real practical purpose?

Properties are setting observable values. You could pull at any time (synchronously) a library compatible current value. Streams are sequences of events. They aren't synchronously by nature. The library gives you the means to evaluate it imperatively. The events are uniquely and irrevocably bound to their generation in the context of the universe.