Lazy sampling allowing the removal of buildDynamic, introducing liftPushM#529
Lazy sampling allowing the removal of buildDynamic, introducing liftPushM#529parenthetical wants to merge 4 commits intoreflex-frp:developfrom
buildDynamic, introducing liftPushM#529Conversation
There was a problem hiding this comment.
Pull request overview
This PR updates Reflex’s Spider implementation to support lazy sample semantics compatible with MonadFix, and refactors the MonadHold API by removing buildDynamic in favor of a new liftPushM method (with buildDynamic retained as a deprecated helper).
Changes:
- Implement lazy
sampleforSpiderusingunsafeInterleaveIO, plus a new per-frame queue to force sampled thunks to WHNF before propagation. - Remove
buildDynamicfrom theMonadHoldclass API and introduceliftPushMto keep equivalent functionality expressible. - Add micro tests covering the
MonadFixsampling scenarios that previously could trigger “cycles in fixIO”.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
test/Reflex/Test/Micro.hs |
Adds regression tests for MonadFix + sample ordering scenarios. |
src/Reflex/Spider/Internal.hs |
Implements lazy sampling + thunk-forcing queue; removes dynamic-init machinery tied to buildDynamic; adds liftPushM instances. |
src/Reflex/Pure.hs |
Refactors holdDyn/buildDynamic behavior for the Pure timeline; adds liftPushM. |
src/Reflex/Profiled.hs |
Updates MonadHold instance to work with liftPushM (and no longer implements buildDynamic). |
src/Reflex/PerformEvent/Base.hs |
Threads through the new liftPushM method for PerformEventT. |
src/Reflex/Class.hs |
Removes buildDynamic as a class method, adds liftPushM, adds default method signatures, and reintroduces buildDynamic as a deprecated helper. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
94af26a to
52484eb
Compare
52484eb to
9436fde
Compare
|
I tried to make a non-breaking
|
|
This is very interesting but also quite intricate. Would you mind giving a few examples of
|
|
I've added some new tests in the MR, copied below. Notice how This is not a problem you normally run into, until you do. The creation of the Behavior might be far removed from where the sampling happens, especially if these operations are happening within the implementation of some higher level library. , testB "sampling-monadfix" $ do
rec
x <- sample b
b <- hold "0" never
_ <- sample b
return (x <$ b)
, testB "sampling-monadfix-twice-removed" $ do
rec
e1 <- events1
x <- sample b
b' <- hold x e1
b <- hold "0" e1
_ <- sample b'
return b'Older tests do this: , testB "buildDynamicStrictness" $ do
rec
d'' <- pushDyn return d'
d' <- pushDyn return d
d <- holdDyn "0" =<< events1
_ <- sample (current d'')
return (current d'')where pushDyn :: (Reflex t, MonadHold t m) => (a -> PushM t b) -> Dynamic t a -> m (Dynamic t b)
pushDyn f d = buildDynamic (sample (current d) >>= f) (pushAlways f (updated d))You have to do any sampling in the first argument of As a user you'd need to "just know" that I'm not sure if there are any combinations of |
This now passes the test suite after lazy sampling.
|
I've added a third commit switching to Spider's implementation of The Spider headE now passes the test suite instead of having it hang forever, although I'm not 100% sure whether that's an accident of the test cases or wether the issue is fundamentally solved. |
The more efficient headE implementation was reverted due to this issue in commit 04556b9
Reflex currently requires us to predict whether sampling the "now" value of a behavior operationally precedes the creation of the behavior in
MonadFixinstances. This makes it hard to implement high-level abstractions on top of Reflex. For example, a combination ofDynamicWriterand aReaderof the result can lead to users running into mysterious "cycles in fixIO" errors just by changing the order of innocuous looking expressions inMonadFix. To keep laziness the library implementer would have to addbuildDynamicstyle versions of their primitives, and teach the user how to use these.This MR solves this issue and makes it so that
sampleinSpidermatches thePuresemantics inMonadFix. This is achieved usingunsafeInterleaveIOon theIORefread. The sampled values are forced to WHNF before event propagation to prevent incorrect results. If the values were left unforced theIORefmight contain a future value instead of the one at the logical time of thesample:instance HasSpiderTimeline x => Reflex.Class.MonadSample (SpiderTimeline x) (EventM x) where {-# INLINABLE sample #-} - sample (SpiderBehavior b) = readBehaviorUntracked b + sample (SpiderBehavior b) = do + holdInits <- getDeferralQueue + res <- liftIO . unsafeInterleaveIO $ runBehaviorM (readBehaviorTracked b) Nothing holdInits + enqueueForce res + return resThe MR contains two commits. The first implements lazy sampling without adjusting the
MonadHoldclass, while the second follows up and removes the no longer neededbuildDynamicclass member. To keepbuildDynamicimplementable, I've added a new function toMonadHoldcalledliftPushM.I was hoping the defaults would avoid a breaking change, but unfortunately the extra
Reflex tconstraint on the default implementation breaks downstream after all:Performance implications
If I'm interpreting things correctly the benchmarks didn't show any performance degradation, and running reflex-todomvc looks normal as well.