Announcing Deku - A PureScript Web Micro-Framework

Hey all!

I’m thrilled to announce a new web micro-framework I wrote this weekend. It’s called purescript-deku.

Here are some relevant links:

Deku sits at the confluence of three unique circumstances:

  • I’m building a web instrument for a client where Halogen is taking too many milliseconds from my rendering loop, causing some visual and audio jank.
  • I wanted to take purescript-wags 's underlying comonadic paradigm and apply it to a different domain.
  • I unexpectedly had ten hours of uninterrupted, baby-free time at home to code.

While hacking Deku together, I was obsessively benchmark-focused, making sure that it stayed really fast. I used the halogen-hooks benchmark suite as one point of comparison and have put those results in the README.md. My gut feeling is that it is even faster than React, rivaling Svelte in its performance.

I intend to keep Deku a micro-framework, and I’ve already started a couple additional repos to add functionality, including a Markdown parser and a React-like provider. Both of these are less than 500 loc. If anyone would like to learn Deku by building out some of these peripherals with me, please let me know!

It was a blast to write the Deku docs with Deku, and I’m looking forward to building some more apps with it. If you’re the sort of person that enjoys tinkering with new things, please have a go at the examples in the documentation or modify one of the examples in the examples folder. You can use this starter repo as a point of departure. Feedback is very welcome, and happy hacking with Deku!

AdvancedSnoopyHuemul

11 Likes

Looks quite interesting - I’ll happily play with this a bit

1 Like

Awesome! I’m eager to see what you build with it and hear your feedback :pray:

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).

5 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.

4 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