Building a Haskell web application using Reflex. Part 1

Introduction

Hello everyone! My name is Nikita, and we at Typeable use the FRP approach to develop the frontend for some projects, and specifically its implementation in Haskell – a web framework reflex… There are no manuals on this framework on Russian-language resources (and there are not so many of them on the English-language Internet), and we decided to fix it a little.

This article series will walk you through creating a Haskell web application using the framework reflex-platformreflex-platform provides packages reflex and reflex-dom… Package reflex is an implementation Functional reactive programming (FRP) in Haskell. In library reflex-dom contains a large number of functions, classes and types for working with DOM… These packages are separate because The FRP approach can be used not only in web development. We will develop the application Todo List, which allows you to perform various manipulations with the task list.

A non-zero level of knowledge of the Haskell programming language is required to understand this series of articles, and a prior knowledge of functional reactive programming is helpful.

There will be no detailed description of the FRP approach. The only really worth mentioning are the two main polymorphic types on which this approach is based:

  • Behavior a Is a reactive variable that changes over time. It is a kind of container that contains a value throughout its life cycle.
  • Event a – event in the system. An event contains information that can only be obtained when the event is triggered.

Package reflex provides another new type:

  • Dynamic a – is a union Behavior a and Event a, i.e. it is a container that always contains some value, and, like an event, it knows how to notify about its change, unlike Behavior a

IN reflex the concept of a frame is used – the minimum unit of time. The frame starts with the event that occurs and continues until the data in that event is no longer processed. The event can raise other events received, for example, through filtering, mapping, etc., and then these dependent events will also belong to this frame.

Training

First of all, you need an installed package manager nix… How to do it is described here

To speed up the build process, it makes sense to configure the cache nix… In case you are not using NixOS, then you need to add the following lines to the file /etc/nix/nix.conf:

binary-caches = https://cache.nixos.org https://nixcache.reflex-frp.org
binary-cache-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=
binary-caches-parallel-connections = 40

If you are using NixOS, then the file /etc/nixos/configuration.nix:

nix.binaryCaches = [ "https://nixcache.reflex-frp.org" ];
nix.binaryCachePublicKeys = [ "ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=" ];

For this tutorial, we’ll stick to a standard three-package structure:

  • todo-client – client part;
  • todo-server – server part;
  • todo-common – contains common modules that are used by server and client (for example API types).

Next, you need to prepare the development environment. To do this, repeat the steps from documentation:

  • Create application directory: todo-app;
  • Create projects todo-common (library), todo-server (executable), todo-client (executable) in todo-app;
  • Configure build via nix (file default.nix in the directory todo-app);
    • Also, remember to enable the option useWarp = true;;
  • Configure build via cabal (files cabal.project and cabal-ghcjs.project).

At the time of publication of the article default.nix will look something like this:

{ reflex-platform ? ((import <nixpkgs> {}).fetchFromGitHub {
    owner = "reflex-frp";
    repo = "reflex-platform";
    rev = "efc6d923c633207d18bd4d8cae3e20110a377864";
    sha256 = "121rmnkx8nwiy96ipfyyv6vrgysv0zpr2br46y70zf4d0y1h1lz5";
    })
}:
(import reflex-platform {}).project ({ pkgs, ... }:{
  useWarp = true;

  packages = {
    todo-common = ./todo-common;
    todo-server = ./todo-server;
    todo-client = ./todo-client;
  };

  shells = {
    ghc = ["todo-common" "todo-server" "todo-client"];
    ghcjs = ["todo-common" "todo-client"];
  };
})

Note: the documentation suggests to manually clone the repository reflex-platform… In this example, we used the means nix to get the platform from the repository.

It is convenient to use the tool during client development ghcid… It automatically refreshes and restarts the application when the source changes.

To make sure everything works, add to todo-client/src/Main.hs the following code:

{-# LANGUAGE OverloadedStrings #-}
module Main where

import Reflex.Dom

main :: IO ()
main = mainWidget $ el "h1" $ text "Hello, reflex!"

All development is carried out from nix-shell, so at the very beginning you need to enter this shell:

$ nix-shell . -A shells.ghc

To run through ghcid you need to enter the following command:

$ ghcid --command 'cabal new-repl todo-client' --test 'Main.main'

If everything works, then at localhost:3003 you will see a greeting Hello, reflex!

Why 3003?

The port number is looked for in an environment variable JSADDLE_WARP_PORT… If this variable is not set, the default is 3003.

How it works

You can see that we did not use GHCJSand the usual GHC… This is possible thanks to the packages jsaddle and jsaddle-warp… Package jsaddle provides an interface for JS to work from under GHC and GHCJS… Using the package jsaddle-warp we can start a server that will update via websockets DOM and play the role of a JS engine. This is exactly what the flag was set for. useWarp = true;otherwise the default would be the package jsaddle-webkit2gtk, and on launch, we would see a desktop application. It is worth noting that there are still layers jsaddle-wkwebview (for iOS apps) and jsaddle-clib (for Android applications).

The simplest TODO application

Let’s start developing!

Let’s add the following code to todo-client/src/Main.hs

{-# LANGUAGE MonoLocalBinds #-}
{-# LANGUAGE OverloadedStrings #-}
module Main where

import Reflex.Dom

main :: IO ()
main = mainWidgetWithHead headWidget rootWidget

headWidget :: MonadWidget t m => m ()
headWidget = blank

rootWidget :: MonadWidget t m => m ()
rootWidget = blank

We can say that the function mainWidgetWithHead is an element <html> pages. It takes two parameters – head and body… There are more functions mainWidget and mainWidgetWithCss… The first function only accepts a widget with an element body… The second – the first argument takes the styles added to the element style, and the second argument is the element body

Any HTML element or group of elements will be called widgets. The widget can have its own event network and produces some HTML code. Basically, any function that requires typeclasses responsible for constructing DOM, in the output type, will be called a widget.

Function blank is equivalent pure () and she does nothing, does not change in any way DOM and does not affect the network of events in any way.

Now let’s describe the element <head> our page.

headWidget :: MonadWidget t m => m ()
headWidget = do
  elAttr "meta" ("charset" =: "utf-8") blank
  elAttr "meta"
    (  "name" =: "viewport"
    <> "content" =: "width=device-width, initial-scale=1, shrink-to-fit=no" )
    blank
  elAttr "link"
    (  "rel" =: "stylesheet"
    <> "href" =: "https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"
    <> "integrity" =: "sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh"
    <> "crossorigin" =: "anonymous")
    blank
  el "title" $ text "TODO App"

This function will generate the following element content head:

<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1, shrink-to-fit=no" name="viewport">
<link crossorigin="anonymous" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"
  integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" rel="stylesheet">
<title>TODO App</title>

Class MonadWidget allows you to build or rebuild DOMand also define the network of events that occur on the page.

Function elAttr has the following type:

elAttr :: forall t m a. DomBuilder t m => Text -> Map Text Text -> m a -> m a

It accepts the tag name, attributes, and element content. This function returns, and in general the entire set of functions that build DOM, what their inner widget returns. In this case, our elements are empty, so we use blank… This is one of the most common uses of this function when you want to empty the body of an element. The function is also used el… Its input parameters are only the tag name and content, in other words, this is a simplified version of the function elAttr no attributes. Another function used here is text… Its task is to display text on the page. This function escapes all possible service characters, words and tags, and therefore exactly the text that is passed to it will be displayed. In order to embed a piece of html, there is a function elDynHtml

It must be said that in the above example, using MonadWidget is redundant because this part builds an immutable plot DOM… And, as mentioned above, MonadWidget allows you to build or rebuild DOMand also allows you to define a network of events. The functions used here only require a class DomBuilder, and here, indeed, we could only write this restriction. But in general, there are much more restrictions on the monad, which complicates and slows down development if we write only those classes that we need now. Therefore there is a class MonadWidget, which is a kind of Swiss knife. For the curious, here is a list of all the classes that are superclasses. MonadWidget:

type MonadWidgetConstraints t m =
  ( DomBuilder t m
  , DomBuilderSpace m ~ GhcjsDomSpace
  , MonadFix m
  , MonadHold t m
  , MonadSample t (Performable m)
  , MonadReflexCreateTrigger t m
  , PostBuild t m
  , PerformEvent t m
  , MonadIO m
  , MonadIO (Performable m)
#ifndef ghcjs_HOST_OS
  , DOM.MonadJSM m
  , DOM.MonadJSM (Performable m)
#endif
  , TriggerEvent t m
  , HasJSContext m
  , HasJSContext (Performable m)
  , HasDocument m
  , MonadRef m
  , Ref m ~ Ref IO
  , MonadRef (Performable m)
  , Ref (Performable m) ~ Ref IO
  )

class MonadWidgetConstraints t m => MonadWidget t m

Now let’s move on to the element body pages, but first, let’s define the data type that we will use for the job:

newtype Todo = Todo
  { todoText :: Text }

newTodo :: Text -> Todo
newTodo todoText = Todo {..}

The body will have the following structure:

rootWidget :: MonadWidget t m => m ()
rootWidget =
  divClass "container" $ do
    elClass "h2" "text-center mt-3" $ text "Todos"
    newTodoEv <- newTodoForm
    todosDyn <- foldDyn (:) [] newTodoEv
    delimiter
    todoListWidget todosDyn

Function elClass takes the tag name, class (s) and content as input. divClass this is a shortened version elClass "div"

All of the above functions are responsible for the visual presentation and do not carry any logic in themselves, unlike the function foldDyn… She is part of the package reflex and has the following signature:

foldDyn :: (Reflex t, MonadHold t m, MonadFix m) => (a -> b -> b) -> b -> Event t a -> m (Dynamic t b)

She looks like foldr :: (a -> b -> b) -> b -> [a] -> b and, in fact, performs the same role, only in the role of a list here the event. The resulting value is wrapped in a container Dynamicsince it will update after every event. The update process is set by the parameter function, which takes as input the value from the occurred event and the current value from Dynamic… On their basis, a new value is formed, which will be in Dynamic… This update will occur every time an event occurs.

In our example, the function foldDyn will update the dynamic list of tasks (initially empty) as soon as a new task is added from the input form. New tasks are added to the beginning of the list, because function used (:)

Function newTodoForm builds that part DOM, in which there will be a form for entering a description of the task, and returns an event that carries a new Todo… It is when this event occurs that the list of tasks will be updated.

newTodoForm :: MonadWidget t m => m (Event t Todo)
newTodoForm = rowWrapper $
  el "form" $
    divClass "input-group" $ do
      iEl <- inputElement $ def
        & initialAttributes .~
          (  "type" =: "text"
          <> "class" =: "form-control"
          <> "placeholder" =: "Todo" )
      let
        newTodoDyn = newTodo <$> value iEl
        btnAttr = "class" =: "btn btn-outline-secondary"
          <> "type" =: "button"
      (btnEl, _) <- divClass "input-group-append" $
        elAttr' "button" btnAttr $ text "Add new entry"
      pure $ tagPromptlyDyn newTodoDyn $ domEvent Click btnEl

The first innovation we see here is the function inputElement… Its name speaks for itself, it adds an element input… It takes as a parameter the type InputElementConfig… It has many fields, inherits several different classes, but in this example it is most interesting for us to add the necessary attributes to this tag, and this can be done using a lens initialAttributes… Function value is a class method HasValue and returns the value that is in the given input… In case of type InputElement it is of type Dynamic t Text… This value will be updated every time the field changes. input

The next change you can see here is the use of the function elAttr'… Distinguishing functions with a stroke from functions without a stroke for plotting DOM lies in the fact that these functions, in addition, return the page element itself, with which we can perform various manipulations. In our case, it is needed so that we can receive the click event on this element. This is done using the function domEvent… This function takes the name of the event, in our case Click and the element itself with which this event is associated. The function has the following signature:

domEvent :: EventName eventName -> target -> Event t (DomEventType target eventName)

Its return type depends on the type of the event and the type of the element. In our case it is ()

The next function we meet is tagPromptlyDyn… It has the following type:

tagPromptlyDyn :: Reflex t => Dynamic t a -> Event t b -> Event t a

Its task is to put into it the value that is currently inside when an event is triggered Dynamic… Those. event resulting from a function tagPromptlyDyn valDyn btnEv occurs simultaneously with btnEv, but carries the meaning that was in valDyn… For our example, this event will occur when the button is pressed and carry the value from the input text field.

Here it should be said about the fact that functions that contain the word in their name promptlypotentially dangerous – they can cause loops in the network of events. Outwardly, it will look like the application is frozen. Call tagPromplyDyn valDyn btnEv, if possible, should be replaced by tag (current valDyn) btnEv… Function current gets Behavior of Dynamic… These calls are not always interchangeable. If the update Dynamic and event Event in tagPromplyDyn arise at one moment, i.e. in one frame, then the output event will contain the data received Dynamic in this frame. In case we use tag (current valDyn) btnEv, then the output event will contain the data that the original current valDyn, i.e. Behavior, possessed in the last frame.

Here we come to another difference between Behavior and Dynamic: if a Behavior and Dynamic get updated in one frame, then Dynamic will be updated already in this frame, and Behavior will take on new meaning in the following. In other words, if the event happened at the time t1 and at the moment of time t2then Dynamic will have the value that the event brought t1 in time [t1, t2), а Behavior(t1, t2]

Function task todoListWidget is to display the entire list Todo

todoListWidget :: MonadWidget t m => Dynamic t [Todo] -> m ()
todoListWidget todosDyn = rowWrapper $
  void $ simpleList todosDyn todoWidget

Here comes the function simpleList… It has the following signature:

simpleList
  :: (Adjustable t m, MonadHold t m, PostBuild t m, MonadFix m)
  => Dynamic t [v]
  -> (Dynamic t v -> m a)
  -> m (Dynamic t [a])

This feature is part of the package reflex, and in our case it is used to construct repeating elements in DOMwhere it will be consecutive elements div… She takes Dynamic a list that can change over time and a function to process each item separately. In this case, it’s just a widget for displaying one list item:

todoWidget :: MonadWidget t m => Dynamic t Todo -> m ()
todoWidget todoDyn =
  divClass "d-flex border-bottom" $
    divClass "p-2 flex-grow-1 my-auto" $
      dynText $ todoText <$> todoDyn

Function dynText different from function text the fact that the input takes text wrapped in Dynamic… If the list item is changed, then this value will also be updated in DOM

Also, 2 functions were used, about which nothing was said: rowWrapper and delimiter… The first function is a wrapper over the widget. It is nothing new and looks like this:

rowWrapper :: MonadWidget t m => m a -> m a
rowWrapper ma =
  divClass "row justify-content-md-center" $
    divClass "col-6" ma

Function delimiter just adds a separator element.

delimiter :: MonadWidget t m => m ()
delimiter = rowWrapper $
  divClass "border-top mt-3" blank

The result can be viewed in our repository

That’s all it takes to build a simple and stripped-down application. Todo… In this part, we looked at setting up the environment and proceeded directly to the development of the application itself. The next part will add work with a list item.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *