purescript-event icon indicating copy to clipboard operation
purescript-event copied to clipboard

Event values are dependent on subscription time

Open paf31 opened this issue 7 years ago • 7 comments

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?

paf31 avatar Jun 25 '18 16:06 paf31

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

robertdp avatar Jul 12 '18 02:07 robertdp

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

paf31 avatar Jul 25 '18 19:07 paf31

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)

robertdp avatar Jul 26 '18 03:07 robertdp

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.

paf31 avatar Aug 14 '18 20:08 paf31

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 avatar Aug 15 '18 03:08 robertdp

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?

robertdp avatar Aug 17 '18 05:08 robertdp

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

safareli avatar Jul 17 '19 09:07 safareli