cujojs / most

Ultra-high performance reactive programming
MIT License
3.49k stars 206 forks source link

".map" over console.log does not seem to show the stream #432

Closed dmitriz closed 7 years ago

dmitriz commented 7 years ago

Summary

The following code does not seem to show the stream:

from([1,2]).map(x=>console.log(x))

Expected result

Expected: 1 followed by 2 in the console

Actual Result

In the Node REPL:

Stream {
  source: Map { f: [Function], source: ArraySource { array: [Object] } } }

Versions

node@6.9.2

Steps to reproduce

Node REPL

node
>most = require('most')
>most.from([1,2]).map(x=>console.log(x))
//=> Stream {
  source: Map { f: [Function], source: ArraySource { array: [Object] } } }

Code to reproduce

TylorS commented 7 years ago

Hello @dmitriz thanks for the issue. In most.js, as well as other stream libraries, there is a general property that streams are lazy or in other words do nothing until they are activated. In most.js there are a few operators to do so, but the most common are observe and drain. I think observe is what you're looking for here.

// with observe
most.from([1, 2]).observe(x => console.log(x))
// the way observe(f) is *actually* implemented using tap(f) + .drain()
most.from([1, 2]).tap(x => console.log(x)).drain()

Hope this helps!

davidchase commented 7 years ago

@dmitriz You can check out the documentation for consuming streams here: https://github.com/cujojs/most/blob/master/docs/api.md#consuming-streams

dmitriz commented 7 years ago

Thank you @TylorS for the explanation and @davidchase for the reference.

It feels a bit confusing as this behaviour differs form other stream libraries such as flyd or mithril/stream. I knew that was how observable behave but didn't expect it from streams.

Any use cases when this might be beneficial over the always-active approach by flyd?

It would really help (at least to me) to have some more detailed explanation of what "consuming" precisely means and which methods do consume and which don't. Right now the section on "Consuming streams" starts with the reduce method that does not say anything about the consumption. The other methods descriptions only say "Start consuming events from stream" but don't explain what it means...

Also this is not clear to me from https://github.com/cujojs/most/blob/master/docs/api.md#observe: "The returned promise will fulfill after all the events have been consumed"

For instance, if our stream keeps running indefinitely, how can all the events be consumed? What if more events will keep coming in? Shall we still observe them?

What I am really looking for is the equivalent of the map from flyd, where the passed function simply gets called with every emitted value. I understand that observe does not provide this as it returns a Promise, where I really want to return another dependent Stream. Is it possible with most?

Sorry for the naive questions ;)

Frikki commented 7 years ago

@dmitriz As @TylorS explained, observe (f, stream) is really drain(tap(f, stream). f is your provided function that will perform a side effect on each item in the stream. Any return value of f is ignored. tab will then return a new stream with the same items as in the originally provided stream. The new stream is passed to drain, which returns a Promise. This Promise either fulfills when the stream ends without an error or is rejected if the stream ends with an error. Take note that the events of the stream are time-ordered.

Thus, if you have an "infinite" stream, e.g., a source that emits an event periodically, the provided function f will fire on every event, performing whatever side effect specified. Logically, the returned Promise will neither resolve nor reject (granted no errors occurred). After all, the stream is "infinite". If the stream was finite (it ends), the code will then choose the path of either resolved or rejected Promise, if you so choose. As you see, the events are still consumed and passed to f.

You can easily map over the stream as in flyd, but you still need to consume the stream:

const stream = from([1, 2, 3])

const transformedStream = map(add2, stream)

function add2(value) { return value + 2 }

observe(console.log, transformedStream) // 3, 4, 5

Hope this helps (and that I didn’t provide any incorrect information in my explanation).

dmitriz commented 7 years ago

@Frikki Thank for your explanation.

tap will then return a new stream with the same items as in the originally provided stream.

Possibly that is my problem. Can I do it without creating a new stream? Like simply attach a passive observer but keep the stream unchanged and going?

That is what flyd streams do, and then you can pipe values into them via the setter syntax stream(value), which is something I was looking for in most but was unable to find. How can I pipe a value into existing active stream?

Logically, the returned Promise will neither resolve nor reject (granted no errors occurred). After all, the stream is "infinite".

That makes me confused. Does the stream not get forked into finite one, before being passed to the Promise? And is there any benefit of this approach over simply attaching a passive observer that will return the same value as the promise but can also be reused for the future values? That is, what is the benefit of transferring into promise instead of leaving it as stream?

Sorry if those questions are naive, I'm just trying to understand how the things work.

dmitriz commented 7 years ago

To give perhaps some motivation for my question, I have been looking at this very nice example. The approach is based on reaching out for a selector to read from the input stream. However, as e.g. noted in http://www.christianalfoni.com/articles/2016_04_06_CycleJS-driven-by-state :

It is indeed very weird that our view says nothing about how it changes state and our state change logic just has some random class names it listens to. This selector also has some challenges thinking scalability. Imagine an application with hundreds of elements and you depend on classnames to find your elements. It is very fragile. Maybe we can do something about that.

I have to agree with it, as there can be potentially hundreds of selectors but only few action streams to subscribe. So it feels a more scalable approach to invert the control by giving to the vnode the right to dispatch actions into the action stream, instead of the stream itself reaching out for the selectors in the wild. This is also very close to the Redux architecture.

But for this to work, I need the ability to dispatch values into active streams, which should be passed to the subscribers without any extra work. Now I am puzzled how to achieve this with most as I don't see how to activate my stream and how to dispatch into it thereafter.

davidchase commented 7 years ago

Sorry if those questions are naive, I'm just trying to understand how the things work.

@dmitriz your questions are not naive, we are all just trying to learn.

Now I am puzzled how to achieve this with most as I don't see how to activate my stream and how to dispatch into it thereafter.

Im not 100% sure that this is what you want/need but check out our community package Most Subject. This should allow you to create a stream and then push values on it akin to what i believe flyd allows you to do.

let me know if this helps at all 😄

dmitriz commented 7 years ago

@davidchase Many thanks, yes, that is exactly what I was looking for!

That means, I can enable the un library to be powered by the mostjs stream creator factory, and expose the streams for both actions and states from each UI uncomponent. That will allow people already using most to control their un-components with the same stream api.

Would there be interest in such functionality?

davidchase commented 7 years ago

@dmitriz glad that worked for you 👍

That means, I can enable the un library

The library looks interesting i will have to take a closer look in a bit

Would there be interest in such functionality?

What is the functionality you are proposing ?

dmitriz commented 7 years ago

@davidchase

The library looks interesting i will have to take a closer look in a bit

Thank you 😄

I've just expanded the intro and added more examples. Any feedback is welcome!

Would there be interest in such functionality?

What is the functionality you are proposing ?

I have added more details here: https://github.com/dmitriz/un#full-reactive-control-of-your-uncomponents

Let me know if that makes sense or needs to make the point more clear.

briancavalier commented 7 years ago

@dmitriz The original issue here seems to be resolved (observing the stream activates it), and it seems like @davidchase's suggestion of looking at most-subject was helpful for your subsequent questions. So, I'm closing, but please re-open or create a new issue if there's more we need to discuss.

Cheers!