baconjs / bacon.js

Functional reactive programming library for TypeScript and JavaScript
https://baconjs.github.io
MIT License
6.47k stars 330 forks source link

Bugs with stream update #298

Closed Voronchuk closed 10 years ago

Voronchuk commented 10 years ago

Do anyone can explain me why code like this works:

# Stream -> Object
# Send bet action on server
setupBets = ($betAmount) ->
    startButton = prepareObjectForBaconEventTarget d3.select RUN_GAME_SEL
    $startGameClicks = Bacon.fromEventTarget startButton, 'click'

    $request = Bacon.constant(GAME_API_URL).map d3.xhr.bind d3
    $requestData = Bacon.combineTemplate([action: EVENT_BET, context: $betAmount]).map JSON.stringify

    $requestResponse = requestJsonStream [$startGameClicks, $request, $requestData]
    $requestResponseArray = parseJsonStream $requestResponse, EVENT_BET
    $requestResponseArray.log()  # this line start execution

    startGameClicks:        $startGameClicks
    requestResponse:        $requestResponse
    requestResponseArray:   $requestResponseArray

But if I remove $requestResponseArray.log() nothing will happen at all.

Here is logic attached to requestJsonStream, parseJsonStream and prepareObjectForBaconEventTarget

# ArrayOfStreams -> Stream
# Make request for JSON data when you have values in all passed streams
requestJsonStream = ($streams) ->
    Bacon.when $streams, (args..., request, data) ->
        Bacon.fromNodeCallback(request, 'post', data).map('.response').map(JSON.parse).flatMap (json) -> Bacon.sequentially 0, json

# Stream String -> Stream
# Parse stream with raw JSON data to fetch event data objecr
parseJsonStream = ($stream, event) ->
    $stream.flatMap (stream) -> stream.filter((item) -> item.event == event).map '.context'

# (Object String) -> Object
# Add functions to object which are required for Bacon.fromEventTarget
prepareObjectForBaconEventTarget = (obj, event) ->
    obj.addEventListener = obj.on
    obj.removeEventListener = obj.on.bind obj, event, null
    obj
phadej commented 10 years ago

That's the issue @raimohanska mentioned in https://github.com/baconjs/bacon.js/issues/297

If you call only setupBets, but don't use returned streams for anything, then nothing happens. However log makes subscription (i.e. "uses" streams), so data flow is forced, and something happens.

Try testing setupBets like:

s = setupBets
s.requestResponseArray.log()
Voronchuk commented 10 years ago

Thank you, Oleg. I was thinking in a similar way, just wished to be sure.

Mb we need a kind of .run() function if we don't need stream data? What do you think @raimohanska?

raimohanska commented 10 years ago

Adding a .run() or similar been suggested a few times already (even by myself) but we've decided against it.

If you need a forced subscription to make your application work, you've usually "done it wrong". I'm using quotes because what's right/wrong is a matter of opinion. But generally the idea of FRP (according to me, now) is to compose data using streams and properties and assign side effects to these observables to achieve desired effects. If the stream itself is supposed to do the side-effects you're "doing it wrong".

In your case though it seems that we are looking at an unfinished application where the actual subscribers would be added later. I use .log() myself in these cases, before plugging in the actual subscriber.

Feel free to monkeypatch Observable.prototype with .run() if you find that convenient!

Voronchuk commented 10 years ago

Thanks for explanation.