restaumatic / purescript-specular

A Reflex-Dom inspired UI library for PureScript
MIT License
131 stars 10 forks source link

Support for foldDynM-esque function? #54

Open werner291 opened 5 years ago

werner291 commented 5 years ago

Hello!

Any possibility to add support for some kind of foldDyn with monadic output, such as https://github.com/reflex-frp/reflex/blob/6cb58502c930e5eee88d007861881a43b7e2859a/src/Reflex/Dynamic.hs#L144

I'll see if I can throw something together myself, but I'm afraid I don't quite know what exactly I'm doing.

werner291 commented 5 years ago

Please ignore, this is wrong. Turns out I misunderstood what exactly Pull does.

Here's my first attempt (UNTESTED!)

foldDynM :: forall m a b. MonadFRP m => (a -> b -> Pull b) -> b -> Event a -> m (Dynamic b)
foldDynM f initial (Event event) = do
  -- Reference to hold the current value of the output dynamic.
  ref <- liftEffect $ newRef initial

  updateOrReadValue :: Pull b <- liftEffect $
    -- Read the event once per frame, checking for presence, and perform pull action.
    let 
      toPull :: Pull (Effect b)
      toPull = do
        evt <- readBehavior event.occurence
        oldValue <- pullReadRef ref
        case evt of
          Just occurence -> do
            newValue <- f occurence oldValue
            pure $ do
              writeRef ref newValue
              pure newValue
          Nothing -> 
            pure $ pure oldValue
    in
      oncePerFramePullWithIO toPull identity

  unsub <- liftEffect $ event.subscribe $ void $ framePull $ updateOrReadValue
  onCleanup unsub

  pure $ Dynamic
    { value: Behavior updateOrReadValue
    , change: map (\_ -> unit) (Event event)
    }

Do with that what you wish, you probably have a better feel than me for how to integrate this into the codebase. (Assuming it works at all, I out of time to test it today.)

werner291 commented 4 years ago

Ideally, one would be able to run foldDyn inside the fold step function.

zyla commented 4 years ago

@werner291 Hello, thanks for the question!

  1. Note that Specular's Pull is quite different from Reflex PullM. It doesn't offer any performance advantage in current implementation. In fact, I think it was a mistake to expose it in the public API (should have exposed just readDynamic and readBehavior).

  2. Ideally, one would be able to run foldDyn inside the fold step function.

That's a very interesting suggestion. Can you elaborate? (Maybe an example of usage?)

If I'm guessing correctly, you'd like a functionality similar to subscribeDyn (re-running a MonadFRP/MonadWidget computation on some change), but with folding built-in?

werner291 commented 4 years ago

Hello!

Yes, the whole Pull idea didn't remotely work for this.

In my case, I have to deal with a variable number of state machines.

In more formal terms, suppose I have:

Now, I could use foldDyn to make this into a Dynamic (Map k St), but this requires updating the whole Map on every update, which is expensive.

Preferably, I'd like to be able to turn it into some kind of Dynamic (Map k (Dynamic St)) (possibly with a couple monads sprinkled in there). This way, the events can be "routed" to the various sub-dynamics, and anything subscribed to those wouldn't be updated when unrelated events occur with different Id's.

So far, I think this should work https://github.com/werner291/purescript-specular/blob/feature/foldDynM/src/Specular/FRP/Base.purs#L510 Change to the code is pretty minimal (just replaced the let with a monadic bind.)

Haven't had a chance to test it extensively, but this allows calling newDynamic from the Effect. Preferably, though, I'd like to just use foldDyn on new keys and filter on key if possible.

Even better in my particular use-case would be to somehow actually have the FRP plumbing do the routing for you, so that filtering on the tuple's key doesn't touch O(n) dynamics (instead just a O(log n) lookup among the existing state machines, where new ones are created if none exist), but perhaps that's a bit too specific for the library core.

werner291 commented 4 years ago

Perhaps relevant:

fanOut :: forall f k v m. MonadFRP m => Ord k => Traversable f => Event (f (Tuple k v)) -> m (Dynamic (Map k (Event v)))
fanOut ev = do

  {dynamic, read, set} <- newDynamic Map.empty

  flip subscribeEvent_ ev $ \(tuples :: f (Tuple k v)) -> for_ tuples $ \(Tuple k v) -> do
    st <- read
    case lookup k st of
      Just {event,fire} -> 
        fire v
      Nothing -> do
        {event,fire} <- newEvent
        set $ Map.insert k {event,fire} st
        fire v

  pure $ dynamic <#> map _.event

I already did this with effects (untested, but the idea should be clear). It's a bit unwieldy since I'm doing some Effect hackery (also note that events can come in batches in my case, that need not be the case for you).

Also, there is no cleanup here at all.

zyla commented 4 years ago

What you're proposing is interesting, and is in my opinion generic enough to include in the library core. (If we could make the library API powerful enough so that it can be implemented externally - that would be even better!)

However, there's a problem with your foldDynEffect - it allows arbitrary Effects during a frame (here), so you can really mess things up if you e.g. trigger events or modify dynamics inside the folding function. A solution to that would be some kind of restricted monad which allows the operations you want, but no more.

werner291 commented 4 years ago

I see that Relfex has MonadHold.

Perhaps we could take the newDyn bits and factor them out into some kind of Hold monad of our own, that should provide enough to work with.

I can understand arbitrary side-effects are an issue if we allow arbitrary effects.

werner291 commented 4 years ago

Curiously, it seems the Specular design is the opposite of Reflex: Reflex seems to treat hold as the base case, and fold as the more advanced case that builds on that, while Specular implements hold in terms of fold.

werner291 commented 4 years ago

Also, curiously, https://hackage.haskell.org/package/reflex-0.6/docs/Reflex-Class.html#t:PushM contains that fanout functionality that I proposed earlier in the thread.

werner291 commented 4 years ago

It, from what I can tell, is almost exactly what you're proposing: PushM implements limited functionality that allows to do some useful tricks inside the fold step function.

werner291 commented 4 years ago

Could start with something like this:

class MonadFold m where
  -- | `foldDyn f x e` - Make a Dynamic that will have the initial value `x`,
  -- | and every time `e` fires, its value will update by applying `f` to the
  -- | event occurence value and the old value.
  -- |
  -- | On cleanup, the Dynamic will stop updating in response to the event.
  foldDyn :: forall m a b. MonadFRP m => (a -> b -> b) -> b -> Event a -> m (Dynamic b)

Then we implement the regular foldDyn as follows:

instance monadFoldEffectCleanup :: (MonadCleanup m, MonadEffect m) => MonadFold m where
  foldDyn f initial (Event event) = do
    ref <- liftEffect $ newRef initial
    updateOrReadValue <- liftEffect $
      oncePerFramePullWithIO (readBehavior event.occurence) $ \m_newValue -> do
        oldValue <- readRef ref
        case m_newValue of
          Just occurence -> do
            let newValue = f occurence oldValue
            writeRef ref newValue
            pure newValue
          Nothing ->
            pure oldValue

    unsub <- liftEffect $ event.subscribe $ void $ framePull $ updateOrReadValue
    onCleanup unsub

    pure $ Dynamic
      { value: Behavior updateOrReadValue
      , change: map (\_ -> unit) (Event event)
      }

Then we can create another monad that runs in the fold step function that also implements that class.

werner291 commented 4 years ago

https://github.com/werner291/purescript-specular/blob/feature/foldDynM/src/Specular/FRP/Base.purs#L519

How about this?

It has the SimpleFold thing that has a MonadFold instance. Internally, it runs on CleanupT Effect, but as long as you don't export those there shouldn't be any issues.

(NOTE: Still untested, but it seems to be going in the right direction. I can now directly use foldDyn in the step function, it compiles without issues.)