purescript-bonsai

Functional web programming in Purescript. Heavily inspired by Elm & using the Elm Virtual DOM.

Introduction

purescript-bonsai is a functional web programming framework for purescript. It uses Elm’s VirtualDom implementation (the part written in javascript, anyway) and adds the necessary plumbing to make it work with purescript.

The Elm Virtual Dom is tied pretty tightly to how Elm works, so Bonsai follows Elm in a lot of basic design decisions. In particular, the general structure of an application is the same: TEA as in “The Elm Architecture”.

There is a Message type that defines what actions are possible on the Model. The Model is another type that defines the whole state of the application. All changes to this Model go through an update function. The update function applies messages to the current model and produces a new model.

When the model changes, a view function will be called that produces a tree of Virtual Dom nodes. This Virtual Dom tree is then rendered in the browser.

A classical example for a functional web app is a Counter. It displays a number, a “+” button and a “-” button. Clicking the buttons changes the number that is displayed.

Example:

Loading ...

In this case, the Model is simply an Int. The messages can be Inc or Dec:

type Model = Int

data Msg
  = Inc
  | Dec

The update function applies these messages to the current count. It returns a Tuple of command and model. Here the command is empty, but it could also return commands to apply more messages:

update msg model = Tuple empty $
  case msg of
    Inc ->
      model + 1
    Dec ->
      model - 1

The view function produces a tree of Virtual DOM nodes. Note that the model never changes, rather a new model (in this case a new number) is produced. The view function always paints the whole state of the application:

view model =
  render $ div_ $ do
    text $ show model
    button ! onClick Inc $ text "+"
    button ! onClick Dec $ text "-"

I’ve glossed over some things like imports or types or how the application is started. These will be discussed later on, but you can also look at the complete source code of the counter example:

https://github.com/grmble/purescript-bonsai-docs/blob/master/src/Examples/Basic/Counter.purs

Messages and Commands

Messages are applied to the model. The message type defines the possible actions that can change the model.

Commands are a wrapper around these messages, they encode how the messages are delivered. There are pure commands and tasks.

Commands come from two sources: event handlers, and the update function. [1]

Let’s look at an example: you are asked if you want to download some content. If the button is pressed, a progress bar is displayed. Once the animation plays out, the question is shown again.

Example:

Loading ...

Lets start with the message type. There are 2 states we track: if there is a download active, and the progress of that download.

data Msg
  = Progress Number
  | InProgress Boolean

The model is simple as well, it holds the information from the commands:

type Model =
  { progress :: Number
  , inProgress :: Boolean
  }

The update function applies the messages to the model. This update functions simply applies the incoming messages to the model. But it could issue commands as well:

update msg model =
  Tuple empty
    case msg of
      Progress p ->
        model { progress = p }
      InProgress b ->
        model { inProgress = b }

So where are the commands and their messages coming from? As I said, the update function could issue commands, it just does not in this example. In this example, the simulated download is started when the user clicks a button.

view m =
  render $
    div_ $
      if m.inProgress
        then do
          p $ text "Downloading all the things!"
          meter ! cls "pure-u-1-2" ! value (show m.progress) $ text (show (100.0*m.progress) <> "%")
        else do
          p $ text "Would you like to download some cat pictures?"
          div_ $ button
            ! cls "pure-button"
            ! typ "button"
            ! disabled m.inProgress
            ! on "click" (const $ pure $ emittingTask simulateDownload)
            $ text "Start Download"

We have seen examples with onClick. onClick is a convenience function that takes a message and issues a command for it - a pure Command, meaning it will emit that particular message and nothing else.

Here we we don’t want to emit just one message, we want several, with delays in between. So we have to use on. It takes the name of an event (“click”) and an event handling function. This is a function that takes a DOM event and produces a F (Cmd msg). F is from Foreign, it handles failures and gives you do-notation.

(const $ pure $ emittingTask simulateDownload) means: our function will ignore the event (const) and always produce a sucessful F. emittingTask is the Cmd: it is an Aff (think of it like a Thread in other programming languages) that can emit as many messages as it wants because it has a TaskContext:

simulateDownload :: TaskContext Msg -> Aff Unit
simulateDownload ctx = do
  emitMessage ctx (InProgress true)
  for_ (range 1 100) \i -> do
    delay (Milliseconds 50.0)
    emitMessage ctx (Progress $ 0.01 * toNumber i)
  emitMessage ctx (InProgress false)

The other types of tasks are unitTask``(a task that will not emit any messages, it is useful only because of its side effects) and ``simpleTask (can emit exactly one message).

The source code for this example is at https://github.com/grmble/purescript-bonsai-docs/blob/master/src/Examples/Basic/Animation.purs

Footnotes

[1]You can also arrange for commands to be issued from outside via issueCommand

The Update Function

In Elm (or Bonsai), the model of an application contains the complete state of the application at any point in time. The model is immutable. The view function displays the complete model in the browser.

This means that any observable change must be caused by a change in the current model. How does that work, given that the model is immutable?

Bonsai maintains mutable references for the application:

  • a queue of outstanding messages that should be applied to the current model
  • the current model

When commands are emitted, their messages will be queued immediately, and Bonsai will try to apply those messages to the current model as soon as possible. It will call the applications update function with the then current model and the next outstanding message. The update function is responsible for producing the next model state and, optionally, another command.

Updating the model state without issuing any new commands is the common case. The idiom here is Tuple empty. An example would be the update function from our earlier counter example:

update msg model = Tuple empty $
  case msg of
    Inc ->
      model + 1
    Dec ->
      model - 1

A Dec message will subtract 1 from the current counter, a Inc message will add 1. No additional commands have to be emitted, so it wraps the new model in Tuple empty.

In an old version of the animation example, we saw an additional case: a command was issued from the update function. This is accomplished by not returning a plain result, but a real one containing the new model and a (possibly empty) command:

case msg of
 SetText str ->
   Tuple (pureCommand EndAnimation) (model { text = str } )

Bonsai tries hard to apply as many messages as possible between rendering. Once it has applied all queued messages (and all messages emitted by the updates), and has received no additional messages in the mean time, it will schedule a render via requestAnimationFrame. If there still are no unapplied messages in that animation frame, Bonsai will render the model using the view function.

The View Function

The view function is responsible for displaying the model in the browser. It always renders the whole model of the application. Because DOM operations in the browser are moderately expensive, a Virtual DOM is used.

A Virtual DOM is a representation of the DOM tree without any interface to the browser. This representation of the DOM tree can be produced very fast. The Virtual DOM also supports computing a diff between two virtual DOM trees. This diff can then be applied to the real browser’s DOM. Only changed DOM nodes will be touched by this patching operation.

Bonsai provides a Smolder-style syntax to produce virtual DOM nodes. [1] View code is expected to import Bonsai.Html, Bonsai.Html.Attributes and Bonsai.Html.Events. These modules provide helper functions for easily representing HTML content:

view :: Model -> VNode Msg
view model =
  render $ div_ $ do
    text $ show model
    button ! onClick Inc $ text "+"
    button ! onClick Dec $ text "-"

render produces a virtual DOM node from the Smolder-style DSL. div_ and button come from Bonsai.Html, they produce their corresponding HTML elements. Child elements are simply nested in a do block. Attributes and event handlers are specified with ! (this is different from Smolder - Smolder uses different syntax for event handlers).

If an attribute or event handler is not always needed, a Maybe (Property msg) can be put on the element with !?. Elm’s virtual DOM has special helpers for styles, and there is an unconditional and a conditional operator for styles as well: #! and #!? - this is from an old version of the animation example:

p #!? (map (\(Color c) -> style "background-color" c) m.color) $
  text m.text

The styles helper just makes it possible to not provide a single style attribute, but many different styles (some of them conditional). The DSL takes care of producing the final style attribute for you.

Note that with the conditional operators, you usually need map because you have to lift over the structure of the Maybe.

Also note that class properties (cls) are special - if multiple class properties are present, the virtual DOM will join them (separated by a spaces). With all other properties/attributes, later ones overwrite earlier ones.

Footnotes

[1]The HTML Api is optional, you can also work with the VirtualDom directly.

Indices and tables