Deku logo

Core concepts

Providers

Shuttling stuff through the DOM.

Once you start building your game or web app with Deku, you’ll get to the point where you have global information, like authentication status, that you’ll need to pass from the top-level of your application to sub-sub-sub-sub components. Additionally, you’ll need components to be able to communicate with each other. For example, a component five-levels deep may need to initiate a top-level navigation that completely changes the UI.

Deku has no built-in mechanism to deal with these things, but rather encourages certain practices based on classic functional programming patterns. All of these can be thought of as Providers, to borrow a term from React, but with a more powerful and generic API.


Functions as providers

Providers are the beating heart of functional programming because “what does a function do if not provide?” Ok, so that's not an actual quote, I just made it up, and I know it looks awfully pretentious in quotes and all, but it's true, isn't it? The whole point of a function is to provide stuff to its innards and optionally produce a value. So a great way to have React-like contexts and providers in any functional language is to start from the function.

Using functions as monads

As a motivating example, let's create a Deku app that simulates a pre- and post-authentication UI. In React, this would be accomplished with an authentication context and provider. In Deku, we’ll just use a function! And we’ll treat the function as a monad to make the flow more ergonomic.

Our basic authentication app is written thusly.

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

import Prelude
import Deku.Toplevel (runInBody)

import Control.Monad.Reader (ask)
import Data.Tuple.Nested ((/\))
import Deku.Control (text, text_)
import Deku.Core (Nut)
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.Poll (Poll)
import Deku.Toplevel (runInBody)

type Env =
  { isSignedIn :: Poll Boolean
  , setIsSignedIn :: Boolean -> Effect Unit
  }

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 AppMonad = Env -> Nut

signIn :: AppMonad
signIn = do
  { setIsSignedIn, isSignedIn } <- ask
  pure $ D.button
    [ DA.klass_ buttonClass
    , DL.runOn DL.click $ isSignedIn <#> not >>> setIsSignedIn
    ]
    [ text $ isSignedIn <#> if _ then "Sign out" else "Sign in" ]

name :: AppMonad
name = do
  { isSignedIn } <- ask
  pure $ D.td
    [ DA.klass_
        "whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-0"
    ]
    [ text $ isSignedIn <#> if _ then "Mike" else "Nobody" ]

balance :: AppMonad
balance = do
  { isSignedIn } <- ask
  pure $ D.td [ DA.klass_ "whitespace-nowrap px-3 py-4 text-sm text-gray-500" ]
    [ text $ isSignedIn <#> if _ then "42 bucks" else "No money" ]

table :: AppMonad
table = do
  myName <- name
  myBalance <- balance
  pure $ D.table [ DA.klass_ "divide-y divide-gray-300" ]
    [ D.tr_
        [ D.th
            [ DA.scope_ "col"
            , DA.klass_
                "py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-0"
            ]
            [ text_ "Name" ]
        , D.th
            [ DA.scope_ "col"
            , DA.klass_
                "px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
            ]
            [ text_ "Balance" ]
        ]
    , D.tbody [ DA.klass_ "divide-y divide-gray-200" ]
        [ D.tr_ [ myName, myBalance ] ]
    ]

fullApp :: AppMonad
fullApp = do
  mySignIn <- signIn
  myTable <- table
  pure $ D.div_ [ D.div_ [ mySignIn ], D.div_ [ myTable ] ]

main :: Effect Unit
main = void $ runInBody Deku.do
  setIsSignedIn /\ isSignedIn <- useState false
  fullApp { setIsSignedIn, isSignedIn }
NameBalance
NobodyNo money

Like React Contexts and Providers, we can dip into the provider from any component without explicitly passing values through the application. This is because AppMonad expands to the signature Function Env Nut where Env is the context.

In PureScript, Function Env is a monad, which means that its context gets passed down the monadic stack automatically through binds in do notation. Take the following extract from the code above.

app :: AppMonad
app = do
  mySignIn <- signIn
  myTable <- table
  pure $ D.div_ [ D.div_ [ mySignIn ], D.div_ [ myTable ] ]

By left-binding, signIn and table automagically get the Env context passed down through the stack.

If you’re a seasoned functional programmer, you've likely used this pattern in many applications. It's also called the Reader monad. Many functional UI frameworks (including React) have special mechanics to achieve this, but in Deku, the framework is set up in such a way to encourage time-tested functional patterns.

Passing around hooks

As we saw in the first example on this page, we can send the results of hooks - events and pushers - down through our provider system. This provides provisions provokes makes for an elegant and flexible inter-component communication mechanism. However, a case may arise where you accidentally over-wire your system so that you are pushing to a hook that could not possibly active because its element has disappeared. What happens in this case? Let's find out!

The following example is slightly contrived (to be fair, they all are…), but we’ll create a small UI where you have to follow the following steps in order.

  1. Click Cede control.
  2. Click Increment several times.
  3. Click Goodbye
  4. Click Increment again.
View on GithubVITE_START=PassingAroundHooks pnpm example
module Examples.PassingAroundHooks 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 (guard, useHot, useState, useState')
import Deku.DOM.Listeners as DL
import Deku.Toplevel (runInBody)
import Effect (Effect)
import ExampleAssitant (ExampleSignature)
import FRP.Event (keepLatest)
import Deku.Toplevel (runInBody)

main :: Effect Unit
main = void $ runInBody Deku.do
  setIncrementer /\ incrementer <- useState'
  setGoodbye /\ goodbye <- useState true
  D.div_
    [ D.a
        [ DA.klass_ "cursor-pointer"
        , ( DL.runOn DL.click $ keepLatest $ incrementer <#>
              \{ setNumber, number } -> number <#>
                (add 1 >>> setNumber)
          )
        ]
        [ text_ "Increment" ]
    , D.div_
        [ D.a
            [ DA.klass_ "cursor-pointer"
            , DL.click_ \_ -> (setGoodbye false)
            ]
            [ text_ "Goodbye" ]
        ]
    , D.div_
        [ guard goodbye Deku.do
            setNumber /\ number <- useHot 0
            D.div_
              [ D.div_
                  [ text (number <#> show >>> ("n = " <> _))
                  ]
              , D.div_
                  [ D.a
                      [ DA.klass_ "cursor-pointer"
                      , DL.click_ \_ -> setIncrementer { setNumber, number }
                      ]
                      [ text_ "Cede control" ]
                  ]
              ]
        ]
    ]

In this code, we've gotten ourselves into the curious situation where increment is wired up to a listener that can no longer possibly listen: there is no path in the UI that would lead to the counter reappearing.

Thankfully, this is quite rare, and if it does ever arise, the performance impact is minimal as it results in very little CPU being used. That said, 100s of zombie subscriptions will slow down your app, so avoid complex systems like this when possible. Your colleagues will thank you!

Effect systems

Providers are one example of a broader functional pattern called effect systems. In effect systems, there's some collection of side effects that you interpret. The side effect of providers is function application, but we can go wild with whatever effects we want.

Let's create an effect system that contains a provider and a ticker that increments as we create checkboxes. There are several ways to do this in PureScript, but we’ll use a Free Monad in the following example. The nice thing about Free Monads is that you can start small with a couple effects and gradually expand them to an effect system of epic proportions about which functional troubadours will sing for ages to come.

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

import Prelude
import Deku.Toplevel (runInBody)

import Control.Monad.Free (Free, liftF, resume)
import Data.Array (replicate)
import Data.Either (Either(..))
import Data.Traversable (sequence)
import Data.Tuple (Tuple(..))
import Data.Tuple.Nested ((/\))
import Deku.Control (text_)
import Deku.Core (Nut)
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 (cycle, useState)
import Deku.Toplevel (runInBody)
import Effect (Effect)
import ExampleAssitant (ExampleSignature)
import FRP.Event (mapAccum)
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 MyFx a = Reader (Int -> a) | Increment (Unit -> a)

derive instance Functor MyFx

ask :: Free MyFx Int
ask = liftF $ Reader identity

increment :: Free MyFx Unit
increment = liftF $ Increment identity

cbx :: Free MyFx Nut
cbx = do
  increment
  pure $ D.input
    [ DA.xtypeCheckbox
    ]
    []

nuts :: Free MyFx Nut
nuts = do
  i <- ask
  boxes <- sequence (replicate (i + 1) cbx)
  pure $ D.div_
    [ D.p_
        [ text_ "There "
        , text_ $ if i == 1 then "was " else "were "
        , text_ $ show i
        , text_ $ if i == 1 then " checkbox " else " checkboxes "
        , text_ " in the previous frame."
        ]
    , D.div_ boxes
    ]

fx ∷ Int -> Free MyFx ~> Tuple Int
fx a = go a
  where
  go n = resume >>> case _ of
    Left (Reader f) -> go n (f a)
    Left (Increment f) -> go (n + 1) (f unit)
    Right o -> Tuple n o

main :: Effect Unit
main = void $ runInBody Deku.do
  setThunk /\ thunk <- useState unit
  D.div_
    [ D.button
        [ DA.klass_ buttonClass
        , DL.click_ \_ -> setThunk unit
        ]
        [ text_ "Change content" ]
    , cycle (mapAccum fx 0 (thunk $> nuts))
    ]

There were 0 checkboxes in the previous frame.


Row polymorphism

As you build more and more Deku apps, you’ll start to develop components that can be used across projects. You may find yourself building libraries, and these libraries will present APIs to consumers that require them to provide assets like auth tokens using the patterns described above.

When writing a library, you won't have control over the PureScript types used in your consumers' applications, and you’ll want to make it as convenient as possible for consumers to integrate with your service. In these cases, The Deku Way™, and dare I say The PureScript Way™, is to make liberal use of Row Polymorphism. This section is both a crash course in Row Polymorphism as well as some indications of how to use it as a Deku library builder.

Rows and records in PureScript

Rows and records are incredibly powerful concepts in PureScript. Many texts about functional programming and dependent types treat rows as an advanced concept, but PureScript has made them first-class citizens of the language to the point where working with them feels natural and easier than many other language features. If you’re comfortable writing JSON, you should feel at home with rows and records!

A Record in PureScript is defined with curly brackets and has a series of key-value pairs. Here's an example:

{ foo: 1, bar: true }

The type of this record would be the following:

{ foo :: Int, bar :: Boolean }

Records are backed by rows. Rows, indicated by parentheses instead of curly brackets, are kind constructors that allow you to represent sum and product types of arbitrary arity. For example, if one has a row with the following definition:

(foo :: Int, bar :: Boolean)

This could be used to encode a record (a product of these terms, meaning all must be present) or a variant (a sum of these terms, meaning only one is present). Other types can be encoded as well. From a theoretical perspective, rows are so powerful because they represent a potential indexing of the entire domain of a language, from the empty row:

()

To the row of all types:

( int :: Int
, boolean :: Boolean
, string :: String
, arrayInt :: Array Int
...
)

I’m not going to write them all out because it would take ∞ minutes, but convince yourself that one could build a monster row containing every type in a language.

Row polymorphism 101

In the providers above, AppMonad was a record of two key-value pairs. We could have used three pairs, five pairs, or forty-two pairs. That's the beauty of records: they represent an indexing of as many types as you want. And because they can scale up to n types, we can encode that n using polymorphism. In PureScript, when I want to indicate a row that has two certain types plus n other types, where n is between 0 and something really, really big, I write it like this:

forall n. { foo :: Int, bar :: Boolean | n }

The | symbol is telling the compiler that the row behind the record may take other parameters, but this particular function will only be able to access foo and bar.

Thinking back to our goal of writing Deku libraries, if we encode a provider as a polymorphic row, we’re able to construct an API where we specify our dependencies but allow for a row that's larger than what we need. That way, multiple libraries can be blended together, where each one is an index in a row.

Row polymorphism and providers

Now that we've explored what Row Polymorphism looks like, let's see it in the case of a Deku provider. We'll explore two scenarios:

  1. A third-party library called libGreat that's a terminal node in our Deku tree.
  2. A third-party library called libAwesome that's an intermediary node in our Deku tree.

Here's the example.

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

import Prelude
import Deku.Toplevel (runInBody)

import Control.Monad.Reader (ask, asks)
import Data.Newtype (class Newtype, unwrap)
import Deku.Control (text_)
import Deku.Core (Nut)
import Deku.DOM as D
import Deku.Toplevel (runInBody)
import Effect (Effect)
import ExampleAssitant (ExampleSignature)
import Deku.Toplevel (runInBody)

libAwesome
  :: forall n r
   . Newtype n
       { libAwesome ::
           { s1 :: String
           , s2 :: String
           , cont :: n -> Nut
           }
       | r
       }
  => n
  -> Nut
libAwesome = do
  { libAwesome: { s1, s2, cont } } <- asks unwrap
  c <- cont
  pure $ D.div_
    [ D.div__ ("Lib awesome says: " <> s1)
    , D.div__ ("Lib awesome also says: " <> s2)
    , c
    ]

libGreat
  :: forall n r
   . Newtype n
       { libGreat ::
           { x1 :: String }
       | r
       }
  => n
  -> Nut
libGreat = do
  { libGreat: { x1 } } <- asks unwrap
  pure $ D.div_
    [ D.div__ ("Lib great says: " <> x1)
    ]

newtype Env = Env
  { libGreat ::
      { x1 :: String }
  , libAwesome ::
      { s1 :: String
      , s2 :: String
      , cont :: Env -> Nut
      }
  , interjection :: String
  }

derive instance Newtype (Env) _

main :: Effect Unit
main = void $ runInBody Deku.do
  let
    cont = do
      lg <- libGreat
      Env { interjection } <- ask
      pure $ D.div_
        ( [ D.div_ [ text_ interjection ]
          , lg
          ]
        )
  Env
    { interjection: "Oh and..."
    , libAwesome:
        { s1: "I’m awesome!"
        , s2: "Heck yeah!"
        , cont
        }
    , libGreat: { x1: "I’m great!" }
    } # do
    awe <- libAwesome
    pure $ D.div_ [ text_ "In all honesty...", awe ]
In all honesty...
Lib awesome says: I’m awesome!
Lib awesome also says: Heck yeah!
Oh and...
Lib great says: I’m great!

By using Row Polymorphism, both libGreat and libAwesome are able to exist in the Deku tree without knowing much about the environment into which they’re inserted aside from the fact that it's a Record in a newtype. So long as their dependencies are present, they compile.

Because libAwesome refers to additional nodes in the Deku tree, it must provide an environment to these nodes. However, because it cannot know the type of this environment, we’re in a bind. Enter the Newtype constraint. Newtypes allow you to use recursive Row Polymorphism in a library without committing to a concrete type upfront. Armed with this knowledge, you’ll be able to create all sorts of neat Deku libraries. I expect to see the market flooded with Deku image carousel implementations any day now!

Previous
Portals