Deku logo

Functional reactive programming

Effect systems

What wouldn't we do in the name of purity? Probably a lot of stuff… But read on!

So far, our journey through FRP has focused on the pure, unadultered, unblemished, obsidian-black world of Events and Polls. But let's get real - one day, you’re gonna have to make a network call. And when you do, you’ll have to decide between doing something quick and dirty or going down the rabbit hole of effect systems. We hope you choose the second option, and this section is about the many wonderful 🐇🐇🐇 you’ll find there!


Sampling

Before we look at effect systems, it's important to understand sampling in FRP. It's the mildest type of effect - one that grafts the temporality of one event onto another. It's also unlike other effect systems we’ll see later down the line because we can accomplish these directly with combinators over EventandPoll. This section will look at the basic mechanisms for sampling, exploring the various functions available in purescript-hyrule as well as some important corner cases to be aware of.

How sampling works

We'll start our journey through sampling with sampleOnRight aka <|**>. The signature is:

sampleOnRight :: forall a b. event a -> event (a -> b) -> event b

The function listens for changes to the event on the right, recalling the most recent value from the event on the left to produce a b. The event on the left, on the other hand, has no incedence on the emission of b with one exception - it must fire at least once so that b can be produced. That is, until it has fired, emissions of (a -> b) won't result in a b being emitted. Once an a is fired, each subequent (a -> b) will produce a b.

To see this in action, let's concoct an example with two sliders. Both sliders do not emit an initial value, meaning they need to be moved in order for a value to be registered. Because of this, the system will only start producing values once both sliders have been moved, but it will only respond to the right slider. To see this, move the right (nothing), then the left (still nothing), then the right (bam!).

View on GithubVITE_START=HowSamplingWorks pnpm example
module Examples.HowSamplingWorks where

import Prelude
import Deku.Toplevel (runInBody)

import Data.Tuple.Nested ((/\))
import Deku.DOM.Attributes as DA
import Deku.Control (text)
import Deku.Core (fixed)
import Deku.DOM as D
import Deku.Do as Deku
import Deku.Hooks (useState')
import Deku.DOM.Listeners as DL
import Deku.Toplevel (runInBody)
import Effect (Effect)
import ExampleAssitant (ExampleSignature)
import FRP.Event.Class ((<|**>))
import Deku.Toplevel (runInBody)

main :: Effect Unit
main = void $ runInBody Deku.do
  setSlider1 /\ slider1 <- useState'
  setSlider2 /\ slider2 <- useState'
  fixed
    [ D.div [ DA.klass_ "flex justify-around" ]
        [ D.input [ DA.xtypeRange, DL.numberOn_ DL.input setSlider1 ] []
        , D.input [ DA.xtypeRange, DL.numberOn_ DL.input setSlider2 ] []
        ]
    , text
        ( slider1 <|**>
            ((\a b -> show b <> " " <> show a) <$> slider2)
        )
    ]

If you compare the defition of sampleOnRight to that of Poll that we learned in Polls, you’ll see an interesting similarity. When an argument of type Event a is applied to sampleOnRight , it produces a Poll.

type Poll a = forall b. Event (a -> b) -> Event b
type SampleOnRight a =
  Event a -> (forall b. Event (a -> b) -> Event b)

This also allows us to write a definition for the step function we saw in the Polls section.

step :: forall a. a -> Event a -> Poll a
step a e = sampleOnRight (pure a <|> e)

Flipping the temporality

We can flip which event controls time by using sampleOnLeft aka <**|>. The signature is the same as sampleOnRight, namely:

sampleOnLeft :: forall a b. event a -> event (a -> b) -> event b

The example below is the same as the one above save time control - now, the slider on the left controls time. Make sure to move the slider on the right at least once to capture a value!.

View on GithubVITE_START=FlippingTheFunctionAndArgument pnpm example
module Examples.FlippingTheFunctionAndArgument where

import Prelude
import Deku.Toplevel (runInBody)

import Data.Tuple.Nested ((/\))
import Deku.DOM.Attributes as DA
import Deku.Control (text)
import Deku.Core (fixed)
import Deku.DOM as D
import Deku.Do as Deku
import Deku.Hooks (useState')
import Deku.DOM.Listeners as DL
import Deku.Toplevel (runInBody)
import Effect (Effect)
import ExampleAssitant (ExampleSignature)
import FRP.Event.Class ((<**|>))
import Deku.Toplevel (runInBody)

main :: Effect Unit
main = void $ runInBody Deku.do
  setSlider1 /\ slider1 <- useState'
  setSlider2 /\ slider2 <- useState'
  fixed
    [ D.div [ DA.klass_ "flex justify-around" ]
        [ D.input [ DA.xtypeRange, DL.numberOn_ DL.input setSlider1 ] []
        , D.input [ DA.xtypeRange, DL.numberOn_ DL.input setSlider2 ] []
        ]
    , text
        ( slider1 <**|>
            ((\a b -> show b <> " " <> show a) <$> slider2)
        )
    ]

Biasing a side of sampling

Even though sampleOnLeft and sampleOnRight control the temporality of which side to sample from, there's an important corner case to consider: when the events are “cotemporal”.

Of course, in the browser, nothing can happen truly simultaneously on the UI thread - there will always be a before/after relationship. But from the viewpoint of a program, things may seem cotemporal. The easiest way to see this is by using the same event as a source. Let's recreate both examples above using a single slider.

View on GithubVITE_START=BiasingASideOfSampling pnpm example
module Examples.BiasingASideOfSampling where

import Prelude
import Deku.Toplevel (runInBody)

import Data.Tuple.Nested ((/\))
import Deku.DOM.Attributes as DA
import Deku.Control (text)
import Deku.Core (fixed)
import Deku.DOM as D
import Deku.Do as Deku
import Deku.Hooks (useState')
import Deku.DOM.Listeners as DL
import Deku.Toplevel (runInBody)
import Effect (Effect)
import ExampleAssitant (ExampleSignature)
import FRP.Event.Class ((<|**>), (<|*>), (<**|>), (<*|>))
import Deku.Toplevel (runInBody)

main :: Effect Unit
main = void $ runInBody Deku.do
  setSlider /\ slider <- useState'
  fixed
    [ D.div [ DA.klass_ "flex justify-around" ]
        [ D.input [ DA.xtypeRange, DL.numberOn_ DL.input setSlider ] [] ]
    , text
        ( slider <|**>
            ((\a b -> show b <> " " <> show a) <$> slider)
        )
    , D.br_ []
    , text
        ( slider <**|>
            ((\a b -> show b <> " " <> show a) <$> slider)
        )
    , D.br_ []
    , text
        ( ((\a b -> show a <> " " <> show b) <$> slider)
            <|*> slider
        )
    , D.br_ []
    , text
        ( ((\a b -> show a <> " " <> show b) <$> slider)
            <*|> slider
        )
    ]



As you move the slider, a curious situation appears on lines 2 and 4, n'est-ce pas? Even though the events are cotemporal, the second one lags after the first. What gives?

In the browser, one thing always happens after another thing. Technically that's probably true about everything. But let's stick to the browser. In examples 1 and 3 we use left-biased operators, and in examples 2 and 4, we use right-biased operators. In purescript-hyrule, the left-biased operators always emit the left event first, which means that for cotemporal events, the left emits before the right. When we then use the right to control the timing, the left is hydrated with the most recent result.

On the other hand, the right-biased operators always emit the right value before the left one. So when we use the right to control the timing, the left does not have the most recent value yet and we perceive a lag.

So which one is better? You decide! The framework lets you opt into whichever poll suites your needs.


Sampling the impure

Sometimes, you’ll want to sample something impure, like an API or radioactive emissions. This section will show a few strategies when purity is not an option (and let's be real, is it ever an option?).

Sampling a poll with an event

Let's start by using an interval to sample an API call. We have two different types of effects here:

  1. The interval is an ex nihilo effect, meaning that it speaks Latin. No, just kidding, it means that it doesn't depend on any other effect. It just happens, sort of like my forgetfulness or the hummus in my fridge disappearing.
  2. The API call is a rider effect. It depends on the interval effect, because we want to poll the API every n seconds.
View on GithubVITE_START=SamplingAPollWithAnEvent pnpm example
module Examples.SamplingAPollWithAnEvent where

import Prelude
import Deku.Toplevel (runInBody)

import Affjax.ResponseFormat as ResponseFormat
import Affjax.Web as AX
import Control.Alt ((<|>))
import Data.Argonaut.Core (stringifyWithIndent)
import Data.Either (Either(..))
import Data.Op (Op(..))
import Deku.Control (text)
import Deku.Toplevel (runInBody)
import Effect (Effect)
import Effect.Aff (error, killFiber, launchAff)
import Effect.Class (liftEffect)
import Effect.Ref as Ref
import ExampleAssitant (ExampleSignature)
import FRP.Event.Time (interval')
import FRP.Poll (sham)
import Fetch (Method(..))
import Deku.Toplevel (runInBody)

main :: Effect Unit
main = do
  fiber <- Ref.new (pure unit)
  apiz <- interval' (withAPICall fiber) 2000
  void $ runInBody do
    text
      ( pure "Fetching..." <|>
          (pure "Here's a random user: " <> sham apiz.event)
      )

  where
  withAPICall :: forall a. _ -> Op (Effect Unit) String -> Op (Effect Unit) a
  withAPICall fiber (Op ff) = Op \_ -> do
    fb <- launchAff do
      f <- liftEffect $ Ref.read fiber
      killFiber (error "cancelling") f
      result <- AX.request
        ( AX.defaultRequest
            { url = "https://randomuser.me/api/"
            , method = Left GET
            , responseFormat = ResponseFormat.json
            }
        )
      liftEffect case result of
        Left err -> ff (AX.printError err)
        Right response -> ff
          (stringifyWithIndent 2 response.body)
    Ref.write fb fiber
Fetching...

Gating events on polls

In the previous section, we saw examples of network calls being pushed to the boundary. But you can push entire effect systems to the boundary. To do that, we’ll use combinators like withTime and withRandoms. We'll stitch them together using function composition.

In the example below, we’ll make a gate using the mouse position. The mouse will pause the flicker when it's on the right side of the screen (sorry if you’re on mobile, no 🐁 for you!), and otherwise the flicker will continue between random hues of a similar color. Aside from my mad HTML rave skillz, the important thing to note here is how two combinators - the mouse and the randomness - are combined into a smöl effect system using function composition.

View on GithubVITE_START=GatingEventsOnPolls pnpm example
module Examples.GatingEventsOnPolls where

import Prelude
import Deku.Toplevel (runInBody)

import Control.Alt ((<|>))
import Data.DateTime.Instant (unInstant)
import Data.Newtype (unwrap)
import Data.Number ((%))
import Deku.Control (text_)
import Deku.DOM as D
import Deku.DOM.Attributes as DA
import Deku.Toplevel (runInBody)
import Effect (Effect)
import ExampleAssitant (ExampleSignature)
import FRP.Event.Time (interval)
import FRP.Poll (sham)
import Deku.Toplevel (runInBody)

main :: Effect Unit
main = do
  let labelMe label = map { label, time: _ }
  pink <- interval 200
  green <- interval 165
  void $ runInBody do
    D.div
      [ DA.klass
          $
            sham
              ( labelMe "bg-pink-300" pink.event <|>
                  labelMe "bg-green-300" green.event
              ) <#> \{ label, time } ->
              if ((unwrap $ unInstant time) % 4000.0 < 2000.0) then label
              else "bg-pink-800"
      ]
      [ text_ "Par-tay!" ]
Par-tay!

Multiplexing

The hyrule library has several helpful combinators for building effect systems at the boundaries of your application. One of them is withMultiplexing, which allows you to mix several combinators. Rather than using it from the library, we’ll define it inline just so you can see how short these combinators can be. The idea is that, as you get more comfortable with them, you can roll your own!

View on GithubVITE_START=Multiplexing pnpm example
module Examples.Multiplexing where

import Prelude
import Deku.Toplevel (runInBody)

import Control.Alt ((<|>))
import Data.Compactable (compact)
import Data.Either (hush)
import Data.Foldable (for_)
import Data.Functor.Contravariant (cmap)
import Data.Op (Op(..))
import Deku.Control (text)
import Deku.Toplevel (runInBody)
import Effect (Effect)
import ExampleAssitant (ExampleSignature)
import FRP.Event.Time (interval', withDelay)
import FRP.Poll (sham)
import Deku.Toplevel (runInBody)

withMultiplexing
  :: forall a b
   . Array (Op (Effect Unit) b -> Op (Effect Unit) a)
  -> Op (Effect Unit) b
  -> Op (Effect Unit) a
withMultiplexing l op = Op \a ->
  for_ l \f -> let Op x = f op in x a

main :: Effect Unit
main = do
  let
    ms = 967
    loop = 16 * ms
    beat w t op = withDelay (t * ms) (cmap (hush >>> (_ $> w)) op)
    beats =
      [ beat "Work it" 0
      , beat "Make it" 1
      , beat "Do it" 2
      , beat "Makes us" 3
      , beat "Harder" 8
      , beat "Better" 9
      , beat "Faster" 10
      , beat "Stronger" 11
      ]
  dj <- interval' (withMultiplexing beats) loop
  void $ runInBody do
    text $ (pure "Wait for it") <|> sham (compact dj.event)
Wait for it
Previous
Polls