Open werner291 opened 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.)
Ideally, one would be able to run foldDyn
inside the fold step function.
@werner291 Hello, thanks for the question!
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
).
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?
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:
Event (Tuple k v)
St
, transition function step :: v -> St -> St
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.
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.
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 Effect
s 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.
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.
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
.
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.
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.
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.
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.)
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#L144I'll see if I can throw something together myself, but I'm afraid I don't quite know what exactly I'm doing.