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 b
s 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 flap
ping 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:
- The poll may never return a result, in which case our event is muted.
- 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.
VITE_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 Poll
s! 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 Poll
s 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 Poll
s on either side of the applicative as independent. Other Applicative
s 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.
VITE_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.
VITE_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.
VITE_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 Event
s, Poll
s come supercharged with several instances that make working with them easier.
Polls as monoids
Poll
s, can be append
ed if the underlying type is a Semigroup
, and mempty
will generate a pure Poll
around mempty
of the underlying Monoid
.
Polls as Heyting Algebras
Poll
s, 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 Poll
s as a Ring
. Feel free to add, subtract, and multiply them if their underlying type is a Ring
as well!