Core

This package is for core types and logic.

Html Type

data Html m a
  = TextNode Text
  | Node Text [(Text, Prop m a)] [Html m a]
  | Potato (JSM (RawNode, STM (Continuation m a)))

Html m a represents a DOM node. It can represent a text node. It can represent a regular node, with zero or more attributes and zero or more children. It can also represent a Potato, with which we can represent a component built outside of the Shpadoinkle view (e.g. for the purpose of wrapping a component coded in JavaScript).

  • m is an effect (i.e., a monad) to run your event handlers.

  • a is what your event handlers produce. It is the type of the view model.

These are only relevant to event handling. If your view has no event listeners, then m and a can be completely parametric and unconstrained.

In reality, this type is Church encoded for performance reasons. The definition given above is not the actual definition. By applying the Church encoding transformation to the definition given above, we get the actual definition of Html.

Not Virtual DOM
Html is similar to the virtual DOM concept found in frameworks like [React](https://reactjs.org/). In a framework with a virtual DOM, each time the value of the view model changes and the UI needs to be updated, the virtual DOM is recomputed. The virtual DOM contains all the information needed to construct the actual DOM. After the virtual DOM is recomputed, it is diffed against the previous virtual DOM in order to determine what updates (i.e., patches) need to be made to the actual DOM. Html is not a virtual DOM according to this concept of virtual DOMs. Html will NOT be subjected to a DOM diffing algorithm to determine how the actual DOM needs to be updated. Instead, Html will be marshalled into a type which a given backend can use to determine how to update the DOM to reflect a change in the value of the view model. This separation allows the same code to run on any backend.
EndoIso

(Html m) is a Functor, but not Prelude.Functor. It is not functorial in Hask but rather in EndoIso, where the objects are types and the morphisms are EndoIsos.

data EndoIso a b = EndoIso (a -> a) (a -> b) (b -> a)

Prop Type

Nodes (and only Nodes) in Html can have properties. These properties are represented as the Church encoding of the following type, paired with the Text property name:

data Prop m a
  = PText Text
  | PListener (RawNode -> RawEvent -> JSM (STM (Continuation m a)))
  | PFlag Bool

As you may recall, Html has a constructor:

Node :: Text -> [(Text, Prop m a)] -> [Html m a] -> Html m a

Node takes a list of properties: [(Text, Prop m a)]. The Text of a property should match the property key in standard JavaScript. For example, the following are morally equivalent:

const div = document.createElement("div");
div.className = "foo";
Node "div" [("className", PText "foo")] []
Do Not use constructors directly!

It is recommended you do not use these constructors, but rather use the exported named functions, e.g.:

h "div" [("className",  textProp "foo")] []

The reason for this guidance is that we reserve the right to change the constructors of Prop and Html in the future.

Listeners

The listener constructor is PListener, which has the following type:

PListener :: (RawNode -> RawEvent -> JSM (Continuation m a)) -> Prop m a

The raw listener will always receive the RawNode, which is the target of the event; and the RawEvent, which is the event object itself. Both of these newtypes are JSVal. This is needed so that you can still do low-level work; in practice it is expected you would use functions that allow you to ignore these raw components.

Continuation

The type of a state update in Shpadoinkle. A Continuation builds up an atomic state update incrementally in a series of stages. For each stage we perform a monadic I/O computation and we can get a pure state updating function. When all of the stages have been executed we are left with a composition of the resulting pure state updating functions, and this composition is applied atomically to the state.

Additionally, a Continuation stage can be a Rollback action which cancels all state updates generated so far but allows for further state updates to be generated based on further monadic I/O computation.

Also, a Continuation stage can be a Merge action which applies all state updates generated so far (in an atomic transaction) but allows for further state updates to be generated based on further monadic I/O computation. Effectively this allows multiple continuations to be strung together one after another as a single continuation with the same result as if the two continuations were run separately.

The functions generating each stage of the Continuation are called with states which reflect the current state of the app, with all the pure state updating functions generated so far having been applied to it, so that each stage "sees" both the current state (even if it changed since the start of computing the Continuation) and the updates made so far. The updates generated by the continuation are not committed to the real state until the Continuation finishes or reaches a Merge action, at which point the updates are all done in an atomic transaction.

data Continuation m a
  = Continuation (a -> a) (a -> m (Continuation m a))
  | Rollback (Continuation m a)
  | Merge (Continuation m a)
  | Pure (a -> a)
Do not use the constructors directly!

It is recommended that you do not use the constructors for Continuation. Instead you can use the functions provided to construct continuations, such as pur, done, impur, kleisli, causes, causedBy, merge, before, and after. You can also build continuations using the ContinuationT monad transformer. The reason for not using the constructors for Continuation directly is that we reserve the right to change them.

Backend Class

This is an interface for renders of Html:

class Backend b m a | b m -> a where
  type VNode b m
  interpret :: (m ~> JSM) -> Html (b m) a -> b m (VNode b m)
  patch     :: RawNode -> Maybe (VNode b m) -> VNode b m -> b m (VNode b m)
  setup     :: JSM () -> JSM ()

This interface lets you plug into various rendering systems. So long as you can provide implementations of these three functions, you can use shpadoinkle to get an application out of Html.

This packages does not come with a backend implementation, and an implementation is required to run the shpadoinkle function.

Monad Transformer

b is expected to be a Monad Transformer, though this is not required; in practice, (b m) must have an instance of MonadJSM.

VNode

This type family points maps to the underlying representation native to the backend:

type VNode b m

In the case of binding to a JavaScript library, this would most likely be a newtype of JSVal. When binding to a typed implementation, this should just be set to the library type.

Interpret

This function describes how to marshal between Html and the native representation (i.e. VNode):

interpret
  :: (m ~> JSM) (1)
  -> Html (b m) a (2)
  -> b m (VNode b m) (3)

The interpret function can be Monadic, as it is likely going to require IO to obtain the native representation.

1 Interpret is provided with a mechanism for getting from the end user provided Monad to JSM directly.
2 The Html Shpadoinkle view that needs to be marshalled to the native representation for this backend.
3 A Monadic action that generates VNode.

Patch

This function describes how updates are handled:

patch
  :: RawNode (1)
  -> Maybe (VNode b m) (2)
  -> VNode b m (3)
  -> b m (VNode b m) (4)

The interpret function can be Monadic, as it is likely going to require IO to apply the new VNode to the view.

1 This is the parent DOM Node that contains the application. RawNode is a newtype of JSVal.
2 The previously rendered VNode. On the first rendering of the application, this will be Nothing.
3 The VNode the user would like to render.
4 A Monadic action that actually renders in the browser and returns a new VNode. The returned (v :: VNode) will be (Just v) for 2 in the next render.

Setup

This is an optional IO action to perform any initial setup steps a given backend might require:

setup
  :: JSM () (1)
  -> JSM ()
1 This is a callback you are responsible for executing after the setup process is complete. The callback is the entire application. If you do not evaluate the JSM (), then nothing will happen.

In the case of JavaScript-based backends, it will likely include steps like adding the library to the <head> of the page, or instantiating a JavaScript class.

The TVar

The interface for driving the view is software transactional memory (STM).

The Haskell ecosystem has many options for thread safe data structures. Many of these containers can be marshalled to the humble TVar. Theoretically, you could write instances for containers such as IORef, Event t, and Auto m

The TVar is part of ensuring Shpadoinkle applications compose with one another as well as surrounding code. Consider a scenario where there is an existing piece of code that taps into a data stream and logs it:

territory <- newTVarIO mempty (1)

_ <- forkIO . runConduit (2)
            $ readLogFile
           .| takeC 200
           .| mapMC (\s -> atomically $ modifyTVar territory $ currentLog .~ s) (3)
           .| mapM_C processFurther

shpadoinkle id runSnabbdom territory mempty view getBody (4)
1 Create a TVar of the frontend model.
2 Some existing code uses Conduit to read a log file.
3 Now, to show each Log as it passes through, simply write it to the TVar, setting it with a Lens.
4 Start the application. Changes to the territory will be reflected in the view.

This makes integrating the frontend state machine into existing work fairly easy, because often existing locations in the code can be used to update the TVar. You can also listen for state changes originating from inside the Shpadoinkle application using existing machinery such as retry from STM. Here is an example of how to listen for changes to a TVar called model:

do current <- readTVarIO model
   next <- atomically $ do
     current' <- readTVar model
     if current' == current
       then retry
       else return current'
   -- do what should happen when model changes; next contains the new value of model
   doSomething next

Shpadoinkle

There is one application primitive, the shpadoinkle function. It is where these different components come together and describes how they interrelate:

shpadoinkle :: forall b m a. Backend b m a => Monad (b m) => Eq a
  => (m ~> JSM) -> (TVar a -> b m ~> m) -> TVar a -> (a -> Html (b m) a) -> b m RawNode -> JSM ()
shpadoinkle toJSM toM model view stage = setup @b @m @a $ do (1)

  c <- j stage (2)
  initial <- readTVarIO model
  n <- go c Nothing initial (3)
  () <$ shouldUpdate (go c . Just) n model (4)

  where

  j :: b m ~> JSM
  j = toJSM . toM model

  go :: RawNode -> Maybe (VNode b m) -> a -> JSM (VNode b m)
  go c n a = j $ patch c n =<< interpret toJSM (view a)
1 Run the setup for the backend.
2 Get the DOM Node on which to append the view.
3 Pass the initial model to the view function, then convert the Html m to VNode b m.
4 Render the initial VNode b m.
5 Set up go to run whenever shouldUpdate. go renders subsequent states.

Everything else is built on top of this to simplify different setups.