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-platform
… reflex-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 unionBehavior a
andEvent a
, i.e. it is a container that always contains some value, and, like an event, it knows how to notify about its change, unlikeBehavior 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) intodo-app
; - Configure build via
nix
(filedefault.nix
in the directorytodo-app
);- Also, remember to enable the option
useWarp = true;
;
- Also, remember to enable the option
- Configure build via
cabal
(filescabal.project
andcabal-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 meansnix
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 GHCJS
and 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 DOM
and 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 DOM
and 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 Dynamic
since 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 promptly
potentially 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 t2
then 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 DOM
where 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.