Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

An interesting Applicative-ish way of combining Events #245

Open
ocharles opened this issue Feb 4, 2022 · 4 comments
Open

An interesting Applicative-ish way of combining Events #245

ocharles opened this issue Feb 4, 2022 · 4 comments

Comments

@ocharles
Copy link
Collaborator

ocharles commented Feb 4, 2022

Hola,

I just found this fairly interesting little newtype:

{-# language BlockArguments #-}
{-# language DeriveFunctor #-}
{-# language DerivingStrategies #-}

module Reactive.Banana.Step ( Step, runStep, step ) where

-- reactive-banana
import Reactive.Banana
  ( Applicative( liftA2 )
  , Behavior
  , Event
  , Moment
  , MonadMoment
  , (<@>)
  , filterJust
  , liftMoment
  , mergeWith
  , never
  , stepper
  )


newtype Step a = Step { unStep :: Moment (Behavior (Maybe a), Event a) }
  deriving stock Functor


instance Applicative Step where
  pure a = Step $ pure (pure (pure a), never)

  Step f <*> Step x = Step do
    (fB, fE) <- f
    (xB, xE) <- x
    return (liftA2 (<*>) fB xB, filterJust (mergeWith go1 go2 go3 ((,) <$> xB <@> fE) ((,) <$> fB <@> xE)))
    where
      go1 (a, g) = g <$> a
      go2 (g, a) = ($ a) <$> g
      go3 (_, g) (_, a) = Just $ g a -- Simultaneous events, so forget history entirely.


step :: Event a -> Step a
step e = Step do
  b <- stepper Nothing $ Just <$> e
  return (b, e)


runStep :: MonadMoment m => Step a -> m (Event a)
runStep = liftMoment . fmap snd . unStep

The idea is we can use Applicative to combine events by zipping new event occurences with previous occurences for other events.

Here's an example of it in use:

testNetwork :: Event (Either a b) -> MomentIO (Event (a, Char, b))
testNetwork e = runStep $ liftA3 (,,) (step e1) (pure '!') (step e2)
  where
    (e1, e2) = split e
> interpret testNetwork [Just (Left " "), Just (Left "a"), Just (Right True), Just (Left "b"), Just (Left "d"), Just (Right False)]
[Nothing,Nothing,Just ("a",'!',True),Just ("b",'!',True),Just ("d",'!',True),Just ("d",'!',False)]

Here we see no event is emitted for the first two frames, because we haven't yet seen an occurance of the Right event. As soon as that happens (frame 3) we're able to return a triple of: the last seen Left event, the pure ! character, and the latest Right event. The applicative then continues to "hold" previous values for all events as new events come in.

This is especially nice when we want an event to fire when multiple events have occured, as we can now use ApplicativeDo:

  onDone <- runStep do
    beforePick <- step aoiBeforePick.onImageAcquired
    afterPick  <- step aoiPickLocationAfterPick.onImageAcquired
    pickResult <- step aoiAfterPick.onAOI
    centeredPick <- step aoiCorrection.onAOI
    afterPlace <- step aoiPlacement.onImageAcquired
    partCenteringResult <- step onPartCentered

    return PNPResult{..}

Here I want to build up a PNPResult structure by holding various events. If these were Behaviors it would be easier, but that's not what I'm producing. I wanted a way to build the Event applicatively, and this seems to be the magic solution! Thanks to #220, this is now possible.

If this is deemed useful, we could always add it in to reactive-banana itself.

@ocharles
Copy link
Collaborator Author

ocharles commented Feb 4, 2022

@ChrisPenner observed on Twitter that this is basically https://www.learnrxjs.io/learn-rxjs/operators/combination/combinelatest from RxJS. Fun!

@mitchellwrosen
Copy link
Collaborator

Opinion: on balance I think I am in favor of including cool types like this in reactive-banana proper, rather than a companion library. While it's nice to have a minimal core, coming up with things like Step and Tidings on one's own is far from easy, and if they help users solve real problems in standardized ways, I think we ought to promote them.

As a counterpoint, though, look how sprawling the reflex API is: https://hackage.haskell.org/package/reflex. Part of what I think draws people to reactive-banana is its simplicity.

The challenge of course is to decide where to draw the line, and indeed the line may be at "no additional types beyond Event and Behavior". But if we decide to start adding more syntactic sugar like Step, then I think we just need to find the right place in the module hierarchy, and ideally come up with a compelling, real-world example to include in the documentation.

@dpwiz
Copy link

dpwiz commented Feb 27, 2023

I'm afraid the Step name is not informative enough. I couldn't get a clue on what is it about until I've read the whole snippet.
It hints about stepper usage, but not the way it is used and what to expect 🤔

Perhaps Latest would be an improvement?

  onDone <- combineLatest do
    beforePick <- latest aoiBeforePick.onImageAcquired
    ...

I like the idea though. Looks handy for implementing various drag-n-drops (:

@mitchellwrosen
Copy link
Collaborator

Another option for types like this and Tidings is to live in a blessed companion package like reactive-banana-extras or reactive-banana-contrib, which has a less-minimal core, but is still full of high-quality stuff, with an API that we make sure doesn't get weird and unwieldly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants