Announcing Deku - A PureScript Web Micro-Framework

Is there an architectural reason why Deku is faster than the alternatives? I noticed in the readme you say

Deku is short for “DOMs Emitted as Kan-extended Universals.”

is this some deep insight which the category-feary are missing out on? Is there a simple explanation?

1 Like

In Deku’s architecture (and in the architecture of PureScript wags, of which it is copy-pasta), all of the nodes in the DOM are known at compile time. That means that, when an update occurs, only the node in question is ever touched. Under the hood, Deku emits an instruction “modify these nodes” and then passes that instruction to the renderer. The renderer is just one giant object of nodes indexed by whatever they’re labeled as at compile time, so lookup is a simple JS object lookup and modification is direct. There’s no application monad to interpret, no tree to traverse while rendering, no runtime diffing, etc.

I cooked up this example to show what I mean:

https://deku-10-sliders.surge.sh/

In it, every time you move the sider, one instruction is emitted: to update the text node at the bottom. And only one call is made to the DOM - element.innerText = newText. Nothing else is touched.

module Main where

import Prelude

import Data.Array ((..))
import Data.Either (Either(..))
import Data.Foldable (for_)
import Data.Int (toNumber)
import Data.Map (lookup, fromFoldable, insert)
import Data.Maybe (fromMaybe)
import Data.Tuple.Nested ((/\))
import Data.Typelevel.Num (D10)
import Data.Vec (Vec, fill)
import Deku.Change (ichange_)
import Deku.Control.Functions (iloop, (@!>))
import Deku.Control.Types (Frame0, Scene)
import Deku.Create (icreate)
import Deku.Graph.Attribute (cb)
import Deku.Graph.DOM ((:=), root)
import Deku.Graph.DOM as D
import Deku.Graph.DOM.Shorthand as S
import Deku.Interpret (class DOMInterpret, makeFFIDOMSnapshot)
import Deku.Run (defaultOptions, run)
import Deku.Util (vex)
import Effect (Effect)
import FRP.Event (subscribe)
import Web.DOM (Element)
import Web.DOM.Element (fromEventTarget)
import Web.Event.Event (target)
import Web.HTML (window)
import Web.HTML.HTMLDocument (body)
import Web.HTML.HTMLElement (toElement)
import Web.HTML.HTMLInputElement (fromElement, valueAsNumber)
import Web.HTML.Window (document)

nSliders' = 10
nSliders = toNumber nSliders'

data UIEvents = SliderMoved Int Number

scene
  :: forall env dom engine res
   . Monoid res
  => DOMInterpret dom engine
  => Element
  -> Scene env dom engine Frame0 UIEvents res
scene elt =
  ( \_ push ->
      ( icreate $ root elt
          {sliders : D.div []
              ( vex
                  $ map (\i -> D.div []
                      { i: D.input
                          [ D.Xtype := "range"
                          , D.OnInput := cb \e -> for_
                              ( target e
                                  >>= fromEventTarget
                                  >>= fromElement
                              )
                              ( valueAsNumber
                                  >=> push <<< SliderMoved i
                              )
                          ]
                          {}
                      }
                  ) (fill identity :: Vec D10 Int)
              )
            , info: D.div [] {
              sliderSum: D.text ("Sum of all sliders: " <> show (50.0 * nSliders))
            }
          }
      )
        $> (50.0 * nSliders /\ fromFoldable (map (\x -> x /\ 50.0) (0 .. (nSliders' - 1))))
  ) @!> iloop \e _ (sm /\ mp) -> case e of
    Left _ -> pure (sm /\ mp)
    Right (SliderMoved i new) -> let
        orig = fromMaybe 0.0 $ lookup i mp
        nv = sm + new - orig
      in
        ichange_
          { "root.info.sliderSum": "Sum of all sliders: " <> show nv
          } $> (nv /\ (insert i new mp))

main :: Effect Unit
main = do
  b' <- window >>= document >>= body
  for_ (toElement <$> b') \elt -> do
    ffi <- makeFFIDOMSnapshot
    subscribe
      ( run (pure unit) (pure unit) defaultOptions ffi
          (scene elt)
      )
      (_.res >>> pure)

The category theory monicker is just to shoehorn something that sounds fancy into Deku, although I think it actually might sort of means something based on my fuzzy understanding of Kan extensions. And if All Concepts are Kan Extensions, then by definition it can’t not be a Kan extension, so it’s a safe bet!

3 Likes

So similar in nature to SDOM, but with more work pushed to compile time?

Yes, exactly. All of the diffing is done at compile time, so at runtime you only ever have a JS object lookup, which is roughly O(1).

I didn’t know about SDOM, but I’m not surprised Phil made it given his interest in comonads, events and jets. I’m a heavy user of @paf311 's libs - his The Future Is Comonadic! - Speaker Deck was a big inspiration for me diving into comonads, events, and behaviors, all of which are fundamental in both wags and deku.

There is also a dynamic diffing algo in the repo called Tumult which is copied from wags (I literally copy and pasted the repo and just deleted stuff, so there’s lots of cruft left over). It’s also fast-ish, but I’m not sure it’s useful. I can’t think of a case when you’d need runtime diffing, but if one ever emerges, it’s there & usable (there’s an example of it here: purescript-deku/examples/tumult at main · mikesol/purescript-deku · GitHub).

6 Likes

I’ve added a type-level HTML parser and a few little helper functions.

Here’s an example of where it stands.

And the code

module Deku.Example.Toplevel where

import Prelude

import Deku.Change (change)
import Deku.Control.Functions (u)
import Deku.Graph.Attribute (cb)
import Deku.Graph.DOM ((:=))
import Deku.Graph.DOM as D
import Deku.Graph.DOM.Shorthand as S
import Deku.Pursx ((~!))
import Deku.Toplevel ((🚀))
import Effect (Effect)
import Type.Proxy (Proxy(..))

main :: Effect Unit
main =
  ( \push -> u $
      ( (Proxy :: _ """
<div ~mydiv~>
  <h1>Hello!</h1>
  <p>This is what a no-frills deku app looks like.</p>
  <p>It is powered by pursx, a html-like format inspired by JSX.</p>
  <h2>Why Deku</h2>
  <ul>
    <li>It's fast.</li>
    <li>
      Well, that's about it for now... it's fast,
      but perhaps it has other advantages!
    </li>
  </ul>
  <p>
    <span style="font-weight:800;">Gratuitous demo alert:</span>
      click the button below to change the background of this div.
  </p>
  ~mybutton~
</div>
""") ~!
          { mydiv: []
          , mybutton: D.button
            [ D.OnClick := cb (const $ push unit) ]
            (S.text "Click me to change the background color")
          }
      )
  ) 🚀
     \_ -> const $ change
        { "root.psx.mydiv": D.div'attr
            [ D.Style := "background-color: rgb(195,212,209);" ]
        }

If you mess up the HTML, the parser emits an error message showing where the error is:

7 Likes

Really nice features (type-level parser for instance) !

I’m beginning to use deku and I’m trying to deal with the following puzzle :

I’m using https://katex.org/ to translateTeX into HTML, and I’d like to import (inject?) the result into the body of my deku page. They provide 2 possible javascript functions :

  • one that returns a string from a string ("\sqrt{3}" → “<span>…long HTML sequence …</span>”), and
  • the other that returns null from a string and an element (and modifies the element content).

I see several strategies :

  • using the second function (how to extract an element in deku ?)
  • using the first function and modify innerHTML attribute of a deku element (do you plan to provide it in deku ?)
  • parsing the result of the first function and dynamically injecting it in the scene (is it currently possible ?).

What strategy would you suggest to deal with that ?

2 Likes

The most straightforward way would be to use the Self property on any element to grab it and then typeset use Katex that way.

Example: https://deku-katex.surge.sh/
GH: GitHub - mikesol/purescript-deku-starter at katex

main :: Effect Unit
main = runInBody
  ( D.span
      (bang (D.Self := Katex.render "c = \\pm\\sqrt{a^2 + b^2}"))
      []
  )

If you wanna do something fancy like make tooltips over specific bits of the equation or make some parts clickable, you’d need to use a DOM Parser on the string returned by Katex and write a PS function to generate Deku from that. Lemme know if that’s what you’re after & I can help you set it up!

4 Likes

Wow this is so much simpler than what I would have guessed! Thanks Mike, that’s exactly what I wanted.
As for a DOM parser, this would be a real challenge for me and I would love to know how to do that since my main goal is to create some interactive documents for my students (What did you accomplish with PureScript in 2019? - #6 by Ebmtranceboy).
But I’m a slow learner, I need to figure out what I really want to accomplish and, as soon as I get stuck, I’ll call for help, thanks for your time!

2 Likes

Your links didn’t work without JavaScript enabled. It’s probably a good idea to include some sort of <noscript> behavior.

Thanks for reporting that back! If it’s not too much trouble, could you put a list of links that don’t work? I can clean them up. Thanks!

1 Like

Basically everything from the actual document doesn’t work, like the main navigation. It’s not technically invalid, but <a> without href is a smell. I would expect all navigation to have refs to the real pages, and then when JavaScript is on, the onclick would preventDefault and go through the client-side router instead. Not only would this save users with JavaScript disabled for privacy/security, but would also allow users to middle/right-click to open in a new tab/window, cover TUI browsers (if you’ve ever had X11 break, this is a life saver), and help with SEO as the links actually reference something.

5 Likes

Thanks for the detailed explanation!

I’ve updated the deku docs so that they are split over several .html pages. I tested it with JS disabled and it work in Chrome. For example, you can visit the hello world page at its own URL with JS disabled and navigation should work.

The strategy that I used in the docs is pretty lightweight, so it can hopefully be reused in other projects.

2 Likes

Awesome work! However, it appears you need a base URL of /purescript-deku/ so your slugs work in these listed contexts.

2 Likes

Thanks for spotting this and pointing it out! The issue was the the docs are deployed in two different locations that have two different paths as slugs. I’ve now updated it so that it works in both locations.

One thing I should have mentioned - SSR for pursx is still rudimentary and does not handle nested components yet. If anyone would like to take a crack at implementing that, I can give some guidance. It’s pretty straightforward, but it’ll take a few hours to fully implement & write tests for.

1 Like

With the SSR, I could actually see myself having some use cases for this :smile: I’ve always been pretty anti-JSX so as long as the functions still work, that’s good by me. I’m always happy to help to make sure <noscript> is covered.

2 Likes

Yup, the functions work fine!
Lemme know if you wind up using it :slight_smile: And if you feel like hacking at the Pursx SSR lemme know and I can walk you through that part of the code base.

3 Likes

How does Deku rival Svelte in performance? Is this because Deku, like Svelte, is reactive, and does not maintain a virtual DOM? What else is similar/different between Svelte and Deku?

1 Like

Deku, like Svelte, is reactive and does not have a virtual DOM. But where it goes pound-for-pound in Svelte is via pursx.

In Svelte, they use an HTML-like templating language that gets compiled to HTML where possible so that, under the hood, it can do div.innerHTML = myHTML. For something with thousands of static nodes, this will be faster than JS.

Deku does the same thing via pursx. When you have:

type MyDiv = """
<div>Hello world!</div>
"""
myDeku = (Proxy :: _ MyDiv) ~~ {}

Deku takes the contents of MyDiv and uses this as the innerHTML content of its enclosing tag. Like in Svelte, the HTML is validated for correctness at compile-time (in Deku’s case, by the PureScript compiler).

Both Svelte and Deku have hooks into the templating language to allow for dynamic attributes and reactive components. The innerHTML method is still used, but little bits of the resulting DOM are then hooked up to the reactive system, which is still faster than creating it all in JS for sufficiently large chunks of DOM.

Another framework to compare Deku to is Solid.js. Solid is more for app-building and has a similar reactive design philosophy to Deku. Where Deku shines (in my immodest unhumble opinion) is its use of the FRP event-based architecture all the way down, which opens up the expressivity of FP without losing the speed of Solid/Svelte.

4 Likes

What does this mean? What do Solid and Svelte do differently?

1 Like

The Svelte compiler uses the $ character to order reactive blocks. This is a pretty fragile system that requires manually ordering things and is not compositional: you can’t stash to $ sections together and compose them later on. Even in their docs, they mention how the compiler performs static analysis and instructs people on how to write these blocks in order to achieve certain outcomes.

In Deku, on the other hand, all values are reactive events, which allows you to compose them in arbitrary ways. As an example:

D.div_ (map (\i -> text (show <$> filter (_.z == i) myEvent)) (0 .. 10))

And then if you push to whatever is feeding myEvent, ie:

pusher { z: 3, foo: "bar" }
pusher { z: 5, foo: "baz" }

etc., the 3 slot will update for 3, the 5 slot will update for 5, etc. Just from a single map and filter. In Svelte, this type of functional expressivity is just not possible with the compiler and its static analysis.

As for Solid, it doesn’t have this problem because it’s more JS-y, so there’s no special compiler (it’s a standard JSX compiler) and you can do anything in JS that you can do in PS of course. But the issue in Solid is one of keys: for example, the For tag requires you to key everything a la React, which has two issues:

  • You need to come up with some sort of key management system.
  • The presence and absence of elements is controlled by inserting into a sorted collection and then removing from that collection, which creates a mini VDOM, which can get slow for large collections.

Events solve both problems:

  • If an element is created by an event instead of associated to a key, then it can listen for arbitrary “kill me” events and unsubscribe itself when you don’t need it anymore. That reduces the complexity of managing dynamic collections - you can check out ie the todo mvc in the Deku docs, which has 0 key management because it uses (what I believe to be) a better abstraction.
  • Events unsubscribe themselves, so you don’t need to sort them or traverse a sorted list to unsubscribe them. This makes collections super snappy compared to Solid’s method of checking for keys’ referential equality.
8 Likes