Core concepts
Effects
A lesson on inverting control, or "noctlor" (that's control inverted).
If you’re familiar with React or Halogen, the word effect is often used to refer to one of two related concepts:
In both frameworks, the mere presence or absence of a component can trigger all sorts of side effects like REST API calls or robot arms moving. Deku offers no such accoutrements (sorry, robots). Instead, side effects are completely initiated by a component's enclosing scope.
Consider the following typical flow in a garden-variety web app:
- Someone clicks on a button, revealing a component.
- This component has some effectful initialization code.
- When the component goes off the page, it also has some effectful teardown code.
In Deku, those effects are the responsibility of the parent component. If there's no parent, it's the responsibility of the app's initialization code.
This section will explore Deku's effect philosophy and also present some strategies to use when you just can't shake those old React/Halogen effectful habits (we've all been there, you’re not alone).
Hydration
In Deku, components traditionally manage side effects by hydrating their children. Let's look at that what that means and how to implement it in practice.
An alternative effect model
First, a note on why Deku ❤️ hydration. Deku was originally developed to build games and musical instruments. Because of that, the contributors developed a culture of components being as “dumb” as possible. This means that, when a component mounts or unmounts from an application, the act of appearing or disappearing should have minimal impact on the UI.
Why the austerity? Because if you’re writing a game and aiming for 60 FPS, you can't mess around. Random side-effects here and there add up and tank performance. If you’re reading this documentation, it's likely because you too are obsessively anti-jank. Deku wages this crusade against jank at every level of a project, and the docs below will give you helpful hints on how to do it.
Injecting dependencies
Let's explore the Deku-ian effect model with a concrete example. Imagine that you are building an app called Image Roulette that reports when a user is watching or not watching an image. Whenever an image comes into focus, you need to ping the backend that it is being viewed, and whenever an image goes offscreen, you need to report that it is no longer being viewed.
In React, we would likely make a component called SmartImage
that reports its own presence or absence via a REST API call on mount and dismount.
In the following application, we’ll implement a miniature version of Image Roulette with dummy functions for the following API calls:
fetchNewRandomImage
: Fetch a new random image URL and increase the watcher count.decreaseImageWatchCount
: Indicate we are no longer looking at the image.
VITE_START=InjectingDependencies pnpm example
module Examples.InjectingDependencies where
import Deku.Toplevel (runInBody)
import Effect (Effect)
import Prelude
import ExampleAssitant (ExampleSignature)
import Deku.Toplevel (runInBody)
import Data.Int (floor)
import Data.JSDate (getTime, now)
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.Pursx (pursx)
import Deku.Toplevel (runInBody)
import Effect.Aff (Milliseconds(..), delay, launchAff_)
import Effect.Class (liftEffect)
import Effect.Random (random)
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
data UIState
= Beginning
| Loading
| Image { url :: String, watcherCount :: Int }
main :: Effect Unit
main = void $ runInBody Deku.do
let
fetchNewRandomImage = do
delay (Milliseconds 300.0)
n <- liftEffect (getTime <$> now)
c <- liftEffect random
pure $
{ url: "https://picsum.photos/seed/" <> show n <> "/200"
, watcherCount: floor (c * 4200.0)
}
decreaseImageWatchCount _ = do
delay (Milliseconds 300.0)
c <- liftEffect random
pure $ { watcherCount: floor (c * 4200.0) }
setUIState /\ uiState <- useState Beginning
D.div_
[ D.button
[ DA.klass_ buttonClass
, let
fetcher = do
newRandomImage <- fetchNewRandomImage
liftEffect $ setUIState $ Image newRandomImage
loader = liftEffect $ setUIState Loading
in
DL.runOn DL.click $ uiState <#> case _ of
Beginning -> do
launchAff_ do
loader
fetcher
Loading -> pure unit
Image { url } ->
launchAff_ do
loader
_ <- decreaseImageWatchCount url
fetcher
]
[ text $ uiState <#> case _ of
Beginning -> "Get Image"
_ -> "Change Image"
]
, D.div_
[ uiState <#~> case _ of
Beginning -> mempty
Image { url, watcherCount } -> D.div_
[ D.img [ DA.alt_ "Some lorem picsum", DA.src_ url ] []
, D.div_
[ text_ $
"Watcher count (including you): " <> show
watcherCount
]
]
Loading ->
D.div [ DA.klass_ "p-10" ]
[ pursx @Loading {} ]
]
]
type Loading =
"""<div role="status">
<svg aria-hidden="true" class="mr-2 w-8 h-8 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="#7393B3"/>
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/>
</svg>
<span class="sr-only">Loading...</span>
</div>"""
The important thing to retain from this example is that the entire effect lifecycle controlled in the button's click listener. In addition to extricating effectful logic from the image presentation components, this has the added advantage of being able to throttle requests during loading.
Common effectful flows
This section explores common effectful flows, like random number generators and calls to REST APIs, and how to achieve them by triggering these effects from outer scopes.
A time-stamper
In the example below, we mint a very-effectful fresh timestamp every time the text Current timestamp is clicked. The same pattern is accomplishable via useRef
. As is the case with many things in Deku, there is more than one way to skin a Gerudian Lizalfos.
VITE_START=RunningEffectsInResponseToEvents pnpm example
module Examples.RunningEffectsInResponseToEvents where
import Prelude
import Deku.Toplevel (runInBody)
import Data.JSDate (getTime, now)
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 ExampleAssitant (ExampleSignature)
import Deku.Toplevel (runInBody)
main :: Effect Unit
main = void $ runInBody Deku.do
setCurrentTime /\ currentTime <- useState'
D.div_
[ D.a
[ DL.click_ \_ -> (getTime <$> now >>= setCurrentTime)
, DA.klass_ "cursor-pointer"
]
[ text_ "Current timestamp" ]
, text_ ": "
, text (show <$> currentTime)
]
Making API calls
In PureScript, asynchronous effects can be triggered by using launchAff
in an effectful context like a click listener. This allows us to use the same flow from the timestamp example above and adapt it to asynchronous code.
VITE_START=RunningAffsInResponseToAnEvent pnpm example
module Examples.RunningAffsInResponseToAnEvent 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.Aff (launchAff_)
import Effect.Class (liftEffect)
import ExampleAssitant (ExampleSignature)
import Fetch (Method(..), fetch)
import Deku.Toplevel (runInBody)
main :: Effect Unit
main = void $ runInBody Deku.do
setResponse /\ response <- useState'
D.div_
[ D.a
[ DL.click_ \_ -> launchAff_ do
{ text: t } <- fetch "https://httpbin.org/post"
{ method: POST
, body: """{"hello":"world"}"""
, headers: { "Content-Type": "application/json" }
}
t' <- t
liftEffect $ setResponse t'
, DA.klass_ "cursor-pointer"
]
[ text_ "Click for a random http response" ]
, text_ ": "
, text (show <$> response)
]
Waiting for API calls to resolve
In the preivous section, we saw how to launch a one-off effect using launchAff_
. Sometimes, though, we want our ansychronous code to execute in a certain order. For this, there is launchAff
, which returns a Fiber
. These are sort of like JS promises - they are memoized on completion and, when joined in an Aff
, will block until completion. You can use them to run asynchronous code in series.
The following example will emit an aff on each event and the affs will execute one after the other. It does no cancellation, so if affs pile up, they will keep going until the element leaves the screen. If you’re a glutton for punishment, click the link really fast ~20 times while watching your network tab.
VITE_START=RunningAffsSequentiallyInResponseToAnEvent pnpm example
module Examples.RunningAffsSequentiallyInResponseToAnEvent where
import Deku.Toplevel (runInBody)
import Effect (Effect)
import Prelude
import ExampleAssitant (ExampleSignature)
import Deku.Toplevel (runInBody)
import Data.Tuple.Nested ((/\))
import Deku.DOM.Attributes as DA
import Deku.Toplevel (runInBody)
import Deku.Control (text, text_)
import Deku.DOM as D
import Deku.Do as Deku
import Deku.Hooks (useState, useState')
import Deku.DOM.Listeners as DL
import Deku.Toplevel (runInBody)
import Effect.Aff (joinFiber, launchAff, try)
import Effect.Class (liftEffect)
import Fetch (Method(..), fetch)
import Deku.Toplevel (runInBody)
main :: Effect Unit
main = void $ runInBody Deku.do
setResponse /\ response <- useState'
setFiber /\ fiber <- useState (pure unit)
D.div_
[ D.a
[ DL.runOn DL.click $ fiber <#> \f -> setFiber =<< launchAff do
_ <- try $ joinFiber f
{ text: t } <- fetch "https://httpbin.org/post"
{ method: POST
, body: """{"hello":"world"}"""
, headers: { "Content-Type": "application/json" }
}
t' <- t
liftEffect $ setResponse t'
, DA.klass_ "cursor-pointer"
]
[ text_ "Click for a random http response" ]
, text_ ": "
, text (show <$> response)
]
Aff
able asynchronicity in PureScript
Aff
for asynchronous effects. You can think of them as Promises
that are meant to be broken (see Canceler
). liftEffect
converts an Effect
ful computation to an Aff
ish (Aff
ular? Aff
erific?) computation and launchAff_
takes an Aff
and launches it from within an Effect
, making that block run asynchronously.Canceling stale API calls
This variation of the code above does cancellation via the macabre killFiber
, so when a new aff comes down the pipe, the previous one is cancelled.
VITE_START=RunningAffsWithCancellationInResponseToAnEvent pnpm example
module Examples.RunningAffsWithCancellationInResponseToAnEvent where
import Deku.Toplevel (runInBody)
import Effect (Effect)
import Prelude
import ExampleAssitant (ExampleSignature)
import Deku.Toplevel (runInBody)
import Data.Tuple.Nested ((/\))
import Deku.DOM.Attributes as DA
import Deku.Toplevel (runInBody)
import Deku.Control (text, text_)
import Deku.DOM as D
import Deku.Do as Deku
import Deku.Hooks (useState, useState')
import Deku.DOM.Listeners as DL
import Deku.Toplevel (runInBody)
import Effect.Aff (error, killFiber, launchAff)
import Effect.Class (liftEffect)
import Fetch (Method(..), fetch)
import Deku.Toplevel (runInBody)
main :: Effect Unit
main = void $ runInBody Deku.do
setResponse /\ response <- useState'
setFiber /\ fiber <- useState (pure unit)
D.div_
[ D.a
[ DL.runOn DL.click $ fiber <#> \f -> setFiber =<< launchAff do
killFiber (error "Who needed that request?") f
{ text: t } <- fetch "https://httpbin.org/post"
{ method: POST
, body: """{"hello":"world"}"""
, headers: { "Content-Type": "application/json" }
}
t' <- t
liftEffect $ setResponse t'
, DA.klass_ "cursor-pointer"
]
[ text_ "Click for a random http response" ]
, text_ ": "
, text (show <$> response)
]
For game developers
If you’re building a game with Deku, you’re likely used to a different effect model than than the webapp-y one described above. Engines like Unity and Unreal provide two basic functions for GameObject
s (Unity) or Actor
s (Unreal). Using Unreal's terminology, these two functions are:
BeginPlay
: called when play begins for an actor.Tick
: called on each tick of the game loop.
BeginPlay
is of a different ilk than the casual(-ly cruel) one-off effects run on components' mounting and dismounting à la React. It's integral to a game framework's logic. In functional programming, we call this an effect system. That is, it's a predictable contract vis-à-vis side effects that's managed by some interpreter or engine.
We've already seen a Deku-ian effect system in the Providers section. Providers are a category of effects. The specific effect is that of providing information to an otherwise pure component. In this section, we’ll expand upon that notion to create an effect system with a BeginPlay
function. In anger, Deku games often use this strategy.
Creating our effect system
Let's create a little game where a bunch of svg sprites move across the screen. Click them before they turn red to score points! If they turn red, you lose a point. Try not to dip below 0!
VITE_START=Game pnpm example
module Examples.Game where
import Prelude
import Deku.Toplevel (runInBody)
import Control.Alt ((<|>))
import Control.Monad.ST.Class (liftST)
import Control.Monad.ST.Ref (new, read, write)
import Data.Array (drop, take, dropEnd, zipWith)
import Data.Array as Array
import Data.Compactable (compact)
import Data.DateTime.Instant (Instant, unInstant)
import Data.Either (Either(..), either)
import Data.Foldable (sum)
import Data.Int (round, toNumber)
import Data.Maybe (Maybe(..), isJust, maybe)
import Data.Newtype (unwrap)
import Data.Number (floor, sign)
import Data.Number.Format (fixed, toStringWith)
import Data.Op (Op(..))
import Data.Tuple (Tuple(..))
import Data.Tuple.Nested ((/\))
import Deku.Control (text, text_)
import Deku.Core (Nut, useRant)
import Deku.DOM as D
import Deku.DOM.Attributes as DA
import Deku.DOM.Listeners as DL
import Deku.DOM.SVG as DS
import Deku.DOM.SVG.Attributes as DSA
import Deku.Do as Deku
import Deku.Hooks (guardWith, useDynAtEnd, useHot, useState)
import Deku.Toplevel (runInBody)
import Effect (Effect)
import Effect.Random (randomBool, randomRange)
import Effect.Timer (clearTimeout)
import ExampleAssitant (ExampleSignature)
import FRP.Event (Event, fold, mapAccum, sampleOnRight)
import FRP.Event.AnimationFrame (animationFrame')
import FRP.Event.Time (interval', withDelay, withTime)
import FRP.Poll (sham)
import Record (union)
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
type SpriteInit' =
( positionX :: Number
, positionY :: Number
, diameter :: Number
, velocityX :: Number
, velocityY :: Number
, startingTime :: Number
, lifespan :: Number
, bounds :: Number
)
type SpriteInit =
{ canceller :: Effect Unit
| SpriteInit'
}
type SpriteEnv = { time :: Number }
type SpriteInfo =
{ bounds :: Number
, positionX :: Number
, positionY :: Number
, dying :: Maybe (Either Number Number)
, dead :: Maybe Number
, isClicked :: Boolean
, diameter :: Number
, velocityX :: Number
, velocityY :: Number
, startingTime :: Number
, lifespan :: Number
, time :: Number
}
withSpriteDelay
:: Effect Unit
-> Op (Effect Unit) SpriteInit
-> Op (Effect Unit) { | SpriteInit' }
withSpriteDelay scoreDown (Op f) = Op \i -> do
-- change
let
Op wd = withDelay (round (i.lifespan * 1000.0)) $ Op case _ of
Left tid -> f (i `union` { canceller: clearTimeout tid })
Right _ -> scoreDown
wd unit
withSpriteInit
:: Number -> Op (Effect Unit) { | SpriteInit' } -> Op (Effect Unit) Instant
withSpriteInit bounds (Op f) = Op \i -> do
diameter <- randomRange (min bounds 40.0) (min bounds 100.0)
lifespan <- randomRange 3.0 9.0
let radius = diameter / 2.0
let
randPos = randomRange (radius + 1.0)
(bounds - radius - 1.0)
positionX <- randPos
positionY <- randPos
let
randVel = mul <$> randomRange 4.0 40.0 <*>
((if _ then 1.0 else -1.0) <$> randomBool)
velocityX <- randVel
velocityY <- randVel
let startingTime = (unwrap $ unInstant i) / 1000.0
f
{ positionX
, positionY
, diameter
, velocityX
, velocityY
, startingTime
, lifespan
, bounds
}
beginPlay :: SpriteInit -> SpriteInfo
beginPlay
{ positionX
, positionY
, diameter
, velocityX
, velocityY
, startingTime
, lifespan
, bounds
} = do
let
time = startingTime
{ positionX
, positionY
, isClicked: false
, dying: Nothing
, dead: Nothing
, diameter
, velocityX
, velocityY
, time
, startingTime: time
, lifespan
, bounds
}
logisticMap :: Number -> Number
logisticMap x = 3.99 * x * (1.0 - x)
tick :: SpriteEnv -> SpriteInfo -> SpriteInfo
tick env i' = do
let
i = i' { time = env.time }
wither t = do
if env.time - t > 0.5 then do
i { dead = Just env.time, diameter = 0.0 }
else
i { diameter = max 0.0 (i.diameter * (1.0 - ((env.time - t) * 2.0))) }
if isJust i.dead then i
else case i.dying of
Just (Left x) -> wither x
Just (Right x) -> wither x
_ -> do
if i.isClicked then i { dying = Just (Right env.time) }
else do
let dTime = env.time - i'.time
let span = env.time - i.startingTime
if span > i.lifespan then do
i { dying = Just $ Left env.time }
else do
let
posAndVel p v r = do
let np = dTime * v + p
if np < 0.0 || np > i.bounds then do
let nv = (-1.0 * sign np) * r * 36.0 + 4.0
posAndVel p nv (logisticMap r)
else
Tuple np v
rx = env.time - floor env.time
ry = 1.0 - rx
Tuple positionX velocityX = posAndVel i.positionX
i.velocityX
rx
Tuple positionY velocityY = posAndVel i.positionY
i.velocityY
ry
i
{ positionX = positionX
, positionY = positionY
, velocityX = velocityX
, velocityY = velocityY
}
mean :: Array Number -> Number
mean ar =
let
alen = Array.length ar
in
if alen == 0 then 0.0
else (sum ar) / (toNumber alen)
meanDiff :: Array Number -> Number
meanDiff ar = mean $ zipWith (\a b -> 1.0 / ((a - b) / 1000.0)) (dropEnd 1 ar)
(drop 1 ar)
main :: Effect Unit
main = do
let bounds = 1000
unsub <- liftST $ new (pure unit)
af <- animationFrame' withTime
void $ runInBody Deku.do
setGameStarted /\ gameStarted <- useHot Nothing
setScoreBudge /\ scoreBudge <- useState $ Nothing
let
score = fold (\a b -> maybe 0 ((if _ then 1 else (-1)) >>> add a) b)
0
scoreBudge
D.div_
[ D.div_
[ D.button
[ DA.klass_ buttonClass
, DL.runOn DL.click $ gameStarted <#> \s -> do
if isJust s then do
join (liftST $ read unsub)
void $ liftST $ write (pure unit) unsub
setGameStarted Nothing
setScoreBudge Nothing
else do
i <- interval'
( withSpriteDelay (setScoreBudge $ Just false) >>>
(withSpriteInit (toNumber bounds))
)
400
void $ liftST $ write i.unsubscribe unsub
setGameStarted (Just i.event)
]
[ text $ gameStarted <#> isJust >>>
if _ then "Quit" else "Start Game"
]
, guardWith gameStarted \_ -> do
D.div_
[ text_ "Score: "
, text $ show <$> score
, text_ " FPS: "
, text $ toStringWith (fixed 2) <$>
( sham $ mapAccum
(\a b -> Tuple (take 10 ([ b ] <> a)) (meanDiff a))
[]
(map (_.time >>> unInstant >>> unwrap) af.event)
)
]
]
, guardWith gameStarted \emitter -> DS.svg
[ DSA.viewBox_ $ "0 0 " <> show bounds <> " " <> show bounds
, DSA.width_ "400"
, DSA.height_ "400"
]
[ Deku.do
{ value } <- useDynAtEnd
( sham emitter <#> makeFreshSprite (setScoreBudge (Just true))
(af.event <#> _.time)
)
value
]
]
where
makeFreshSprite
:: Effect Unit
-> Event Instant
-> SpriteInit
-> Nut
makeFreshSprite scoreUp animate si = Deku.do
setClicked /\ clicked <- useState false
let bp = beginPlay si
ticked <- useRant $
mapAccum
(\a (Tuple time clk) -> Tuple (tick { time } (a { isClicked = clk })) a)
bp
( sampleOnRight clicked
(Tuple <$> (sham animate <#> unInstant >>> unwrap >>> (_ / 1000.0)))
)
DS.circle
[ DL.click_ \_ -> do
setClicked true
si.canceller
scoreUp
, DSA.cx $ _.positionX >>> show <$> ticked
, DSA.cy $ _.positionY >>> show <$> ticked
, DSA.r $ _.diameter >>> (_ / 2.0) >>> show <$> ticked
, DSA.klass
( pure "fill-blue-500" <|>
( map (either (const "fill-red-500") (const "fill-green-500"))
$ compact
$ (_.dying <$> ticked)
)
)
]
[]
That example is a bit heftier than the other ones, clocking in at around 300 lines of code. But we've created a full-fledged game engine, so that's not that bad. Let's unpack what's going on.
- Just like Unreal, we have
beginPlay
andtick
methods for our Actors. - We use the combinator pattern via
withX
à lahyrule
, the FRP package on which Deku is based, to isolate theEffect
s. - We pile up our sprites as
Nut
s via auseDynAtEnd
. The actors, at the end of the day, are just dynamic elements in a Deku app.
Even more improtantly, it cruises at 60fps and, on my Mac running on battery power with too many tabs open, barely eats up any of the scripting in the rendering loop, leaving ample headroom for experience (clicking around) and whatever else you wanna add to the game.
Bravo!
Take a moment to congratulate yourself for how far you've come. Then, take two moments to congratulate me on toiling over a hot computer for your edification. We've gone from some humble beginnings in Hello world to a full-fledged game!
At the end of the day, Deku is all about making games and musical instruments. Its design tradeoffs favor keeping your game at 60fps over whatever its blind spots are. It does this by eschewing the VDOM model in favor of something more reactive and snappy. At the same time, abstractions like hooks that originated in the VDOM world are easier to grok than reactive programming. When possible, the framework offers these abstractions to its devotees.
Now that you've meticulously poured over and internalized all of the text and examples in this section, you’re pretty far down the FRP rabbit hole. As the light of day recedes, you see that a cranny that may give way to a whole new chapter of this adventure. Against all reason, and certainly against the objections of your colleagues, friends, and family, you decide to keep going.
In the next section, you’re going to learn about Functional Reactive Programming. It will elevate you from Deku Grand Master to an echelon of Deku prowess that is incomprehensible for those who have not attained it. Along the way, you will bend space-time, defy gravity, quantum-entangle and solve a Saturday NYT crossword puzzle. Read on!