reflex-frp / reflex

Interactive programs without callbacks or side-effects. Functional Reactive Programming (FRP) uses composable events and time-varying values to describe interactive systems as pure functions. Just like other pure functional code, functional reactive code is easier to get right on the first try, maintain, and reuse.
https://reflex-frp.org
BSD 3-Clause "New" or "Revised" License
1.06k stars 143 forks source link

Feature request: onDemand :: IO a -> Behavior x a #122

Open achirkin opened 6 years ago

achirkin commented 6 years ago

The point is to be able to run use a cheap IO a function to create a Behavior t a, which would lazily obtain current values whenever any event requires it. I see this question has been answered on stackoverflow from time to time, so I wonder what is the current state of the things?

I've been trying to workaround this for a while and came up with a seems-to-be-not-crashing solution. I posted it as an answer to one discussion. Here is how it looks like:

import System.IO.Unsafe (unsafeInterleaveIO)
import qualified Reflex.Spider.Internal as Spider

onDemand :: IO a -> Behavior t a
onDemand ma = SpiderBehavior . Spider.Behavior . Spider.BehaviorM . ReaderT $ computeF
  where
    {-# NOINLINE computeF #-}
    computeF (Nothing, _) = unsafeInterleaveIO ma
    computeF (Just (invW,_), _) = unsafeInterleaveIO $ do
        toReconnect <- newIORef []
        _ <- Spider.invalidate toReconnect [invW]
        ma

So, the idea is to call invalidate function (with unsafeInterleaveIO) on current state as late as possible every time the value is requested.

Now, the question is if this code breaks something internally or not? :) Related questions:

ryantrinkle commented 6 years ago

Won't this potentially return different two different values if two different events both ask for the value in the same frame? Also, i'm a bit unclear on why unsafeInterleaveIO is required - why not just compute the value directly?

cgibbard commented 6 years ago

Yeah, if we want to do this (and it's likely a good idea more generally), we'll need to cache the result of the IO action and make sure it is executed at most once per frame. The unsafeInterleaveIO is likely a little harder to control than we probably want -- the semantics we'd probably want is that the IO action is executed if and when the behaviour is sampled for the first time in a given frame. This is still rather unsafe from the FRP system's standpoint as it lets you distinguish the order in which behaviours are sampled, but for the purpose of writing hosts, you may be able to live with that in many cases. onDemand certainly shouldn't have such a general type -- this is something which ought to live in a subclass of ReflexHost, just to prevent people from stumbling into using it carelessly. I'd also give it a more awkward name, like newPrimitiveBehavior.

As an aside, at various points where the thing I wanted to produce morally had some type like:

(...) => m (Behavior t X)

where the Behavior requires doing some IO to measure something in the external world to compute, I've found that a decent substitute is often to simulate the attach or attachWith for that instead:

(...) => Event t a -> m (Event t (X, a))

(...) => (X -> a -> b) -> Event t a -> m (Event t b)

since this is how you're likely to have consumed the behaviour anyway.

On Thu, 20 Jul 2017 at 17:42 Ryan Trinkle notifications@github.com wrote:

Won't this potentially return different two different values if two different events both ask for the value in the same frame? Also, i'm a bit unclear on why unsafeInterleaveIO is required - why not just compute the value directly?

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/reflex-frp/reflex/issues/122#issuecomment-316838354, or mute the thread https://github.com/notifications/unsubscribe-auth/AGtSbARyXUplXFoVOhyAn3v2j7rMHwJXks5sP8m7gaJpZM4Od-B9 .

achirkin commented 6 years ago

Thanks for comments!

Surely, I don't pretend my code snipped should go into the library :) Thanks to unsafeInterleaveIO, the action happens when the value of Behavior is evaluated to WHNF, which looks totally non-deterministic. However, this is the only trick I found to invalidate an old cache value: PullSubscribed for a given behavior is created after the action is executed (cause it needs the action result), and unsafeInterleaveIO ensures invalidate is called after that (which would be impossible if you evaluated the content of the action to WHNF at the moment of creating PullSubscribed). Please correct me if this does not make sense!

Now, I am using this workaround in my library to get some really simple state information from JS side of the program, such as whether the CTRL key is pressed at the moment of a click event. So, until reflex library has some smart way to create such behaviors, I would really like to know if my workaround can potentially break some event/switches due to not taking special care about content of toReconnect reference. Is that the case?