paf31 / purescript-event

The Event type, extracted from purescript-behaviors
BSD 3-Clause "New" or "Revised" License
23 stars 18 forks source link

Event values are dependent on subscription time #2

Open paf31 opened 6 years ago

paf31 commented 6 years ago

From https://github.com/paf31/purescript-behaviors/issues/27:

Consider this example:

module Example where

import Prelude

import Control.Monad.Eff (Eff)
import Control.Monad.Eff.Console (CONSOLE, log)
import FRP (FRP)
import FRP.Event (create, subscribe)
import FRP.Event.Class (fold)

main :: forall eff. Eff (frp :: FRP, console :: CONSOLE | eff) Unit
main = do
  {event: rootEvent, push} <- create
  let counter = fold (\_ x -> x + 1) rootEvent 0
  _ <- subscribe counter (\x -> log ("Subscription 1: " <> show x))
  log "Pushing first occurence"
  push unit
  _ <- subscribe counter (\x -> log ("Subscription 2: " <> show x))
  log "Pushing second occurence"
  push unit

Here counter is an Event counting occurences of rootEvent. We subscribe to counter two times. Intuitively, both subscriptions should receive the same values - but that's not what happens. On current master (122e40e08) I get this output:

Pushing first occurence
Subscription 1: 1
Pushing second occurence
Subscription 1: 2
Subscription 2: 1

Is this the intended behaviour?

robertdp commented 6 years ago

The Rx* libraries solve this with the share operator. http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-share

A naive approximation of this is:

share :: forall a. Event a -> Effect (Event a)
share source = do
  { event: shared, push } <- create
  _ <- subscribe source push
  pure shared

Though Observable.share has extra functionality like the shared stream automatically unsubscribing/resubscribing to the source stream based on the number of subscribers the shared stream has.

Update:

Testing with the code

module Main where

import Prelude
import Effect (Effect)
import Effect.Console (log)
import FRP.Event (Event, create, makeEvent, subscribe)
import FRP.Event.Class (fold)

main ::  Effect Unit
main = do
  {event: rootEvent, push} <- create
  counter <- share $ fold (\_ x -> x + 1) rootEvent 0
  _ <- subscribe counter (\x -> log ("Subscription 1: " <> show x))
  log "Pushing first occurence"
  push unit
  _ <- subscribe counter (\x -> log ("Subscription 2: " <> show x))
  log "Pushing second occurence"
  push unit

share :: forall a. Event a -> Effect (Event a)
share source = do
  { event: shared, push } <- create
  _ <- subscribe source push
  pure shared

yields the result

Pushing first occurence
Subscription 1: 1
Pushing second occurence
Subscription 1: 2
Subscription 2: 2
paf31 commented 6 years ago

Ah right, good point. This is probably worth adding as a combinator then, don't you think?

robertdp commented 6 years ago

It's definitely a common use case.

Would a definition like

share :: forall a. Event a -> Effect { event :: Event a, unsubscribe :: Effect Unit }
share source = do
  { event, push } <- create
  unsubscribe <- subscribe source push
  pure { event, unsubscribe }

be sufficient? Or should it be something more complex and self-managing?

I've also got another combinator or two lying around, like merge:

merge :: forall f a. Foldable f => NonEmpty f (Event a) -> Effect (Event a)

On a related note: do you see signals becoming a part of this library, or should they be separate?

newtype Signal a = Signal { event :: Event a, value :: Ref a }

create :: forall a. a -> Event a -> Effect (Signal a)
sample :: forall a. Signal a -> Effect a
transform :: forall a b. (a -> b) -> Signal a -> Effect (Signal b)
paf31 commented 6 years ago

Sorry for the slow reply. Yes, I think share would be a good function to add.

Why merge and not oneOf?

Do you have a use case in mind for signals? I think they should probably be kept separate.

robertdp commented 6 years ago

merge is what most "reactive" JS libraries call the function, and it's basically an effectful append/fold so it made sense. oneOf implies something alternative-based doesn't it?

I started considering wrapping Event to make Signal when I was looking at adding offline functionality to an app. I wanted logic that would fire when the network status transitioned from online to offline and vice versa (no problem thanks to Event), and components to receive the current status when they subscribe. A useful feature was also that I could also sample the current value at any time, for example when a user clicks a button, without needing to introduce a subscription and state somewhere.

Something like:

networkOnline :: Event Unit
networkOffline :: Event Unit

data NetworkStatus = Online | Offline

-- | The browsers current network status, with the initial value of Online.
networkStatus :: Signal NetworkStatus
networkStatus = unsafePerformEffect do
  networkEvent <- Event.merge
      [ const Online <$> networkOnline
      , const Offline <$> networkOffline
      ]
  Signal.create Online networkEvent

This is essentially an Event-based version of Bodil's purescript-signals library, except with no FFI and no implicit effects.

The above code would then give access to the following:

Signal.subscribe networkStatus \status -> ...
-- The current value value is sent to the subscriber immediately, and new values are sent
-- through as they come

Signal.sample networkStatus :: Effect NetworkStatus
-- Allows retrieving the current value from any effectful function

isOnline :: Signal Boolean
isOnline = unsafePerformEffect $ Signal.transform (_ == Online) networkStatus
-- Pointless type clobbering because I can't think of a good example for NetworkStatus
robertdp commented 6 years ago

Okay, so I'm reading through the source of purescript-behaviors [sic] and some of it looks familiar. Is there some way to get this Signal functionality using behaviours?

safareli commented 5 years ago

I think having self managed version of share would be great!