Search code examples
haskellsignalsfrparrowsyampa

Does Yampa have support for memoizing signal functions?


I have written a basic signal function in Yampa as follows:

sf :: SF Int Int
sf = arr $ \x -> trace "1 * 2 = 2" (x * 2)

This function doubles its input and prints "1 * 2 = 2" to the console to indicate that it has been called. I then tested sf with the code below:

embed sf (deltaEncode 1 (repeat 1))

As you can see, only the value of 1 is provided as input to sf. I expected Yampa to have some form of memoization to prevent the repeated calling of sf with the same input, since the function passed to arr should be pure. However, it appears that such support is not available, as "1 * 2 = 2" is printed repeatedly.

To provide additional context, I have some experience with reactive programming using React, and I am currently exploring the concept of FRP. Yampa is my first FRP library of choice, and I was hoping to find something similar to React's memo in Yampa to handle recomputation efficiently. I searched a -> b -> SF a b in Hoogle and found arrPrim. But it didn't work as I expected either.

Is there a way to achieve this in Yampa? If not, how can Yampa execute signal functions efficiently?

Furthermore, the example I provided does not involve time-varying systems, as it simply ignores time. I had initially thought that Yampa could be useful for discrete events-only systems also if it provided something like memo at least. However, if such a thing doesn't exist, then I believe Yampa may only be useful for time-varying systems. Is this understanding accurate?

As I am still in the early stages of learning about FRP, I am concerned that I may have misunderstood some concepts. I would greatly appreciate it if you could clarify any misunderstandings or recommend other FRP libraries that better meet my expectations.


Solution

  • In Haskell, there is no default for memoizing functions. The way Haskell's laziness works is that values are default immutable, so won't be recomputed unless you use a different variable. In this case, the function is memoized, but its results are not. This is because under the hood, you are calling this function on a list of inputs:

    $ ghci
    Prelude> let xs = take 5 $ repeat 1
    Prelude> import Debug.Trace
    Prelude Debug.Trace> let f x = trace "hi" $ x * 2
    Prelude Debug.Trace> f <$> xs
    [hi
    2,hi
    2,hi
    2,hi
    2,hi
    2]
    

    Since each input is distinct, there is no memoization. However, you can memoize your own functions pretty straight forwardly. Simply use a stateful signal function that keeps track of the last input, using sscanPrim:

    -- test.hs
    import FRP.Yampa
    import Debug.Trace
    
    memoized :: Eq a => a -> (a -> b) -> SF a b
    memoized x0 f = sscanPrim memo x0 (f x0)
      where
        memo old new | old == new = Nothing
                     | otherwise = Just (new, f new)
    
    sf :: SF Int Int
    sf = memoized 1 $ \x -> trace "1 * 2 = 2" (x * 2)
    --
    
    $ ghci test.hs
    *Main> take 10 $ embed sf (deltaEncode 1 (repeat 1))
    [1 * 2 = 2
    2,2,2,2,2,2,2,2,2,2]