Deku logo

Functional reactive programming

Polls

How much wood could a woodchuck chuck if a woodchuck could chuck wood?

  • A little
  • A lot
  • Ok wait, this is an example of a “poll”. Ha!

Polls are discrete functions of time that are initiated by other discrete functions of time. Events are discrete functions of time. Sounds like a marriage made in heaven! Or at least in PureScript. On this page, we’ll start by defining the Poll, exploring some of its useful typeclass instances and using it to model various time-domain equations.


Definition

What is a poll? Well, what isn't a poll. Actually, a lot of stuff isn't a poll, so let's just stick with what is. This section will define our new friend Poll, explore the manifold nuances therein, and present a small example of a Poll being used in the browser.

The Poll type

The canonical definition of the poll type is:

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

Like Event, Poll defines a contract. It's saying, “If you give me any event that produces a b in exchange for an a, I’ll give you an event-ful of bs back.” Importantly, to fulfill the contract, you don't know what b is, and yet you have to produce one. The only way we can do this, then, is by applying our function of type a -> b to a.

We consider this a continuous function of time because the poll is defined over the entire lifespan of the event. That is, no matter when Event (a -> b) emits an (a -> b), we need to be able to supply an a. Or, in other words, at this point in time, we need to observe how a is behaving to produce a b. Thus the name poll!

You may be tempted to ask, “But isn't that just flapping over the incoming event to produce the outgoing event?” If so, I encourage you to give in to temptation and ask! And since you asked, the answer is yes. This is a perfectly valid way to create a poll.

always42 :: Poll Int
always42 = poll \e -> e <@> 42

However, this is not the only way to create a poll. Polls encapsulate a broader notion than flapping. Poll also encapsulates two additional related concepts:

  1. The poll may never return a result, in which case our event is muted.
  2. The poll may return multiple values by calling the callback of the resultant event multiple times.

This corresponds to the way observing actually work in the real world. For example, if we send a specialist to observe a daycare, it will take them time to make an observation, they might make several, and there's a non-trivial chance they won't make it out in one piece.

Therefore, even though polls are continuous functions of time, they effectively produce two pieces of information - a new value and a new arity to that value, which at a minimum is nothing and at its maximum is more than you or I can count. Let's focus on the minimum:

proximalObservationOfABlackHole :: Poll Void
proximalObservationOfABlackHole _ = empty

What other type allows you to peek into the void? This is why polls are awesome!

A Poll in the wild

Let's see a poll in the wild! It'll be a little random number generator controlled by Deku, that framework you learned about a few lessons ago that you’re probably already using.

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

import Prelude
import Deku.Toplevel (runInBody)

import Data.Tuple.Nested ((/\))
import Deku.DOM.Attributes as DA
import Deku.Control (text, text_)
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 Effect.Random (random)
import ExampleAssitant (ExampleSignature)
import Deku.Toplevel (runInBody)

buttonClass =
  """inline-flex items-center rounded-md
border border-transparent bg-indigo-600 px-3 py-2
text-sm font-medium leading-4 text-white shadow-sm
hover:bg-indigo-700 focus:outline-none focus:ring-2
focus:ring-indigo-500 focus:ring-offset-2 mr-6""" :: String

main :: Effect Unit
main = do
  n <- random
  void $ runInBody Deku.do
    setNumber /\ number <- useState n
    D.div_
      [ D.button
          [ DA.klass_ buttonClass
          , DL.click_ \_ -> random >>= setNumber
          ]
          [ text_ "Update number" ]
      , text $ number <#>
          show >>> ("Here's a random number: " <> _)
      ]
Here's a random number: 0.46579789771814806

Now, you may be thinking, “wait a second, wasn't that the first example from the State section?” And you'd be right to think that, because it is. Deku runs on Polls! As promised earlier in these hallowed docs, I'd give a shoutout once we got to the Poll section, so consider yourself shout-outed (shot-out? anyone know?).

You may be wondering why Deku doesn't just use events, as Polls seem like they sort of do the same thing. They do, but the crucial difference is that they have an Applicative instance, which Event never will. Remember - only seedy Sunday tabloids can create events out of thin air like batboy or a president's love child: the rest of us need to wait for them to happen. So you can't do pure 42 and presto-chango get a 42 out of an event. But you can do this with a Poll, and we’ll see how and why presently.


Applicative

Polls are applicative functors. Let's see what that means in practice, starting at functors and stopping just shy of monads.

Polls as functors

The definition of Poll's instance of functor is similar to that of Event:

newtype Poll a = Poll (forall b. Event (a -> b) -> Event b)

instance Functor Poll where
  map (Poll b) f = Poll ((lcmap <<< lcmap) f b)

The map applies a transformation to the values at the poll only at the moment that the poll is queried by an event.

Polls as applicatives

Because polls embody a measurement and the potential duration of the measurement as well as the number of times the measurement occurs, the Apply instance of poll needs to accumulate any additional effects added by the two incoming polls. In this way, it is similar to the Apply instance of Event.

A question then arises: is Poll's Apply instance additive or multiplicative? That is, does the Apply instance of Poll accumulate effects?

apply (Poll f) (Poll a) =
  Poll \e -> (map (\ff (Tuple bc aaa) -> bc (ff aaa)) (f (e $> identity))) <*> a (map Tuple e)

Or does it combine them?

apply (Poll f) (Poll a) =
            Poll \e -> a (f (compose <$> e))

Both are valid, but in practice, the additive instance is more useful. This is because we want to think of the two Polls on either side of the applicative as independent. Other Applicatives like Fiber and Event follow the same philosophy.


Calculus

Because polls are continuous functions of time, one can perform classic time-domain operations on them like derivation and integration. You can even solve differential equations using polls. Let's see how!

Derivatives of polls

We can use the function derivative' a b to take the derivative of a positional poll for the slider between 0.0 and 1.0 b with respect to a measure of time a. This is also called velocity. We'll add a small lag to the velocity using fold. This example adds two new helpful functions:

  • step: Starting from an initial value, update a poll based on an event's most recent value.
  • animationFrame: An event that emits on each browser animation frame.
View on GithubVITE_START=DerivingPolls pnpm example
module Examples.DerivingPolls where

import Prelude
import Deku.Toplevel (runInBody)

import Data.Array (drop, length, null)
import Data.DateTime.Instant (unInstant)
import Data.Foldable (sum)
import Data.Int (toNumber)
import Data.Newtype (class Newtype, unwrap)
import Data.Number (isNaN)
import Data.Tuple.Nested ((/\))
import Deku.Control (text)
import Deku.DOM as D
import Deku.DOM.Attributes as DA
import Deku.DOM.Listeners as DL
import Deku.Do as Deku
import Deku.Hooks (useState)
import Deku.Toplevel (runInBody)
import Effect (Effect)
import ExampleAssitant (ExampleSignature)
import FRP.Event (fold, sampleOnRight_)
import FRP.Event.AnimationFrame (animationFrame')
import FRP.Event.Time (withTime)
import FRP.Poll (derivative', sample, sham)
import Deku.Toplevel (runInBody)

newtype Fieldable = Fieldable (Number -> Number)

derive instance Newtype Fieldable _
derive newtype instance Semiring Fieldable
derive newtype instance Ring Fieldable
instance CommutativeRing Fieldable
instance EuclideanRing Fieldable where
  degree _ = 1
  div (Fieldable f) (Fieldable g) = Fieldable \x -> f x / g x
  mod _ _ = Fieldable \_ -> 0.0

instance DivisionRing Fieldable where
  recip (Fieldable f) = Fieldable \x -> 1.0 / f x

main :: Effect Unit
main = do
  af <- animationFrame' withTime
  void $ runInBody Deku.do
    setNumber /\ number <- useState 0.5
    let
      average l
        | null l = 0.0
        | otherwise = sum l / (toNumber $ length l)
      unNaN n = if isNaN n then 0.0 else n
    D.div_
      [ D.div_
          [ D.input
              [ DA.xtypeRange
              , DL.numberOn_ DL.input setNumber
              , DA.klass_ "w-full"
              , DA.min_ "0.0"
              , DA.max_ "1.0"
              , DA.step_ "0.01"
              , DA.value_ "0.5"
              ]
              []
          ]
      , D.div_
          [ text
              ( sham
                  ( average >>> unNaN >>> show <$>
                      ( fold
                          ( \b a ->
                              if length b < 10 then b <> [ a ]
                              else (drop 1 b) <> [ a ]
                          )
                          []
                          ( sample
                              ( derivative'
                                  (pure (Fieldable identity))
                                  ( pure >>> Fieldable <$> sampleOnRight_ number
                                      (sham af.event)
                                  )
                              )
                              ( af.event <#>
                                  _.time
                                    >>> unInstant
                                    >>> unwrap
                                    >>> (_ / 1000.0)
                                    >>> flip unwrap
                              )
                          )
                      )
                  )
              )
          ]
      ]

Integrating polls

We can use the function integrate' a b to take the integral of a positional poll for the slider between 0.0 and 1.0 b with respect to a measure of time a. This simulates a system as if the slider were a gas pedal, the left being your foot off the gas (in this system, no gas means no motion, so imagine a really heavy car going uphill) and 1 being pedal to the metal! The output is the current position.

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

import Prelude
import Deku.Toplevel (runInBody)

import Data.DateTime.Instant (unInstant)
import Data.Newtype (class Newtype, unwrap)
import Data.Tuple.Nested ((/\))
import Deku.Control (text)
import Deku.DOM as D
import Deku.DOM.Attributes as DA
import Deku.DOM.Listeners as DL
import Deku.Do as Deku
import Deku.Hooks (useState)
import Deku.Toplevel (runInBody)
import Effect (Effect)
import ExampleAssitant (ExampleSignature)
import FRP.Event (sampleOnRight_)
import FRP.Event.AnimationFrame (animationFrame')
import FRP.Event.Time (withTime)
import FRP.Poll (integral', sample, sham)
import Deku.Toplevel (runInBody)

newtype Fieldable = Fieldable (Number -> Number)

derive instance Newtype Fieldable _
derive newtype instance Semiring Fieldable
derive newtype instance Ring Fieldable
instance CommutativeRing Fieldable
instance EuclideanRing Fieldable where
  degree _ = 1
  div (Fieldable f) (Fieldable g) = Fieldable \x -> f x / g x
  mod _ _ = Fieldable \_ -> 0.0

instance DivisionRing Fieldable where
  recip (Fieldable f) = Fieldable \x -> 1.0 / f x

main :: Effect Unit
main = do
  af <- animationFrame' withTime
  let pfield = Fieldable <<< pure
  void $ runInBody Deku.do
    setNumber /\ number <- useState 0.0
    D.div_
      [ D.div_
          [ D.input
              [ DA.xtypeRange
              , DL.numberOn_ DL.input setNumber
              , DA.klass_ "w-full"
              , DA.min_ "0.0"
              , DA.max_ "1.0"
              , DA.step_ "0.01"
              , DA.value_ "0.0"
              ]
              []
          ]
      , D.div_
          [ text
              ( sham
                  ( show <$>
                      ( ( sample
                            ( integral' (pfield 0.0)
                                (pure (Fieldable identity))
                                ( pure >>> Fieldable <$> sampleOnRight_ number
                                    (sham af.event)
                                )
                            )
                            ( af.event <#>
                                _.time
                                  >>> unInstant
                                  >>> unwrap
                                  >>> (_ / 1000.0)
                                  >>> flip unwrap
                            )
                        )
                      )
                  )
              )
          ]
      ]

Solving differential equations

Last but not least, we can solve a second order differential equation of the form d^2a/dt^2 = f a (da/dt) using the solve2' function. As the left side is the acceleration of the system, we can solve by integrating twice (using the integrate' function above) after specifying the initial conditions for position and velocity. For example, below we create a damped oscillator using the equation \x dx'dt -> -⍺ * x - δ * dx'dt. In this case, both x (position) and dx'dt (veclocity) are polls.

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

import Prelude
import Deku.Toplevel (runInBody)

import Data.DateTime.Instant (unInstant)
import Data.Newtype (class Newtype, unwrap)
import Data.Tuple.Nested ((/\))
import Deku.Control (text_)
import Deku.DOM as D
import Deku.DOM.Attributes as DA
import Deku.DOM.Listeners as DL
import Deku.Do as Deku
import Deku.Hooks (useState)
import Deku.Toplevel (runInBody)
import Effect (Effect)
import ExampleAssitant (ExampleSignature)
import FRP.Event (keepLatest)
import FRP.Event.AnimationFrame (animationFrame')
import FRP.Event.Time (withTime)
import FRP.Poll (sample, sham, solve2')
import Deku.Toplevel (runInBody)

buttonClass :: String
buttonClass =
  """inline-flex items-center rounded-md
border border-transparent bg-indigo-600 px-3 py-2
text-sm font-medium leading-4 text-white shadow-sm
hover:bg-indigo-700 focus:outline-none focus:ring-2
focus:ring-indigo-500 focus:ring-offset-2 mr-6"""

newtype Fieldable = Fieldable (Number -> Number)

derive instance Newtype Fieldable _
derive newtype instance Semiring Fieldable
derive newtype instance Ring Fieldable
instance CommutativeRing Fieldable
instance EuclideanRing Fieldable where
  degree _ = 1
  div (Fieldable f) (Fieldable g) = Fieldable \x -> f x / g x
  mod _ _ = Fieldable \_ -> 0.0

instance DivisionRing Fieldable where
  recip (Fieldable f) = Fieldable \x -> 1.0 / f x

main :: Effect Unit
main = do
  af <- animationFrame' withTime
  void $ runInBody Deku.do
    let pfield = Fieldable <<< pure
    setThunk /\ thunk <- useState unit
    let
      motion = keepLatest $ thunk $>
        ( sham
            ( map show $ sample
                ( solve2' (pfield 1.0) (pfield 0.0)
                    (pure (Fieldable identity))
                    ( \x dx'dt -> pure (pfield (-0.5)) * x -
                        (pure (pfield 0.1)) * dx'dt
                    )
                )
                ( af.event <#>
                    _.time
                      >>> unInstant
                      >>> unwrap
                      >>> (_ / 1000.0)
                      >>> flip unwrap
                )
            )
        )
    D.div_
      [ D.div_
          [ D.button
              [ DA.klass_ buttonClass, DL.click_ \_ -> (setThunk unit) ]
              [ text_ "Restart simulation" ]
          ]
      , D.div_
          [ D.input
              [ DA.xtype_ "range"
              , DA.klass_ "w-full"
              , DA.min_ "-1.0"
              , DA.max_ "1.0"
              , DA.step_ "0.01"
              , DA.value motion
              ]
              []
          ]
      ]

Granted, these methods may not be immediately useful if you’re using Deku to build a SaaS dashboard or documentation site. But the day you’re hired by the American Calculus Lovers' Association to build their website, you’ll have a nice head start!


Other instances

Like Events, Polls come supercharged with several instances that make working with them easier.

Polls as monoids

Polls, can be appended if the underlying type is a Semigroup, and mempty will generate a pure Poll around mempty of the underlying Monoid.

Polls as Heyting Algebras

Polls, can act as a wrapper around arbitrary Heyting algebras so that you can not, or, and/or and them with reckless abandon.

Polls as rings

Unlike the movie The Ring, a terrible fate will not befall you one week after using Polls as a Ring. Feel free to add, subtract, and multiply them if their underlying type is a Ring as well!