Home Development of Websites Creating a web application in Haskell using Reflex.Part 1

Creating a web application in Haskell using Reflex.Part 1

by admin

Part 2.

Part 3.

Introduction

Hi all, my name is Nikita, and we at Typeable use FRP approach for frontend development for some projects, specifically its implementation in Haskell – webframework reflex There are no manuals for this framework on Russian-language resources (and there aren’t too many on the English-language web either), so we decided to fix that a bit.

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

Creating a web application in Haskell using Reflex.Part 1

Understanding this series of articles requires a non-zero level of knowledge of the Haskell programming language and a prior introduction to functional reactive programming will be helpful.

There won’t be a detailed description of the FRP approach here. The only thing really worth mentioning are the two main polymorphic types on which this approach is based :

  • Behavior a – A time-varying reactive variable. Represents some container that contains a value throughout its lifecycle.
  • Event a – event in the system. An event carries information that can only be retrieved at the time the event is triggered.

Package reflex provides another new type of :

  • Dynamic a – is an amalgamation of Behavior a and Event a which is a container that always contains some value, and, like an event, it knows how to notify its change, unlike Behavior a

In reflex uses the concept of frame, a minimal unit of time. A frame starts when an event occurs and continues until the data in that event is no longer processed. An event can spawn other events, e.g., through filtering, mapping, etc., and then those dependent events will also belong to that frame.

Preparation

The first thing you will need is an installed package manager nix How to do this is described in here

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

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

If you use NixOS, in the file /etc/nixos/configuration.nix :

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

In this tutorial we will stick to the standard structure with three packages :

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

Next, you need to prepare the environment for development. To do this, we have to 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 the assembly via nix (file default.nix in the directory todo-app );
  • Also remember to turn on the option useWarp = true; ;
  • Configure the assembly via cabal (files cabal.project and cabal-ghcjs.project ).
  • At the time of publication of this 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 that the repository should be manually cloned reflex-platform In this example we used the nix to get the platform from the repository.

    During client development, it is convenient to use the tool ghcid It automatically updates and restarts the application when the sources change.

    To make sure that everything works, let’s add in todo-client/src/Main.hs the following code :

    {-# LANGUAGE OverloadedStrings #-}module Main whereimport Reflex.Dommain :: IO ()main = mainWidget$ el "h1" $ text "Hello, reflex!"

    All development is conducted from nix-shell , so you have to enter this shell at the very beginning:

    $ nix-shell . -A shells.ghc

    To start via ghcid the following command is required :

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

    If everything works, at localhost:3003 you will see the welcome message Hello, reflex!

    Creating a web application in Haskell using Reflex.Part 1

    Why 3003?

    The port number is searched for in the environment variable JSADDLE_WARP_PORT If this variable is not set, it defaults to 3003.

    How it works

    You may notice that we did not use the GHCJS but the usual GHC This is possible thanks to the packages jsaddle and jsaddle-warp Package jsaddle provides an interface for JS to work from underneath GHC and GHCJS By using the package jsaddle-warp we can run a server that will, via web sockets, update the DOM and play the role of JS-engine. This is exactly what the flag useWarp = true; otherwise, the default package would be jsaddle-webkit2gtk and we would see a desktop application when we run it. It is worth noting that there are still layers of jsaddle-wkwebview (for iOS applications) and jsaddle-clib (for Android apps).

    The simplest TODO application

    Let’s get to work!

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

    {-# LANGUAGE MonoLocalBinds #-}{-# LANGUAGE OverloadedStrings #-}module Main whereimport Reflex.Dommain :: IO ()main = mainWidgetWithHeadheadWidget rootWidgetheadWidget :: MonadWidget t m => m ()headWidget = blankrootWidget :: MonadWidget t m => m ()rootWidget = blank

    We can say that the function mainWidgetWithHead represents the element <html> page. It takes two parameters – head and body There are also functions mainWidget and mainWidgetWithCss The first function accepts only the widget with the element body The second takes as its first argument the styles added to the element style , and the second argument takes the element body

    We will call a widget as any HTML element, or group of elements. A widget can have its own network of events and produce some HTML code. In fact, any function that requires type classes responsible for building DOM , in the output value type, will be called a widget.

    Function blank is equivalent to pure () and it does not do anything, it does not change DOM and does not affect the event network in any way.

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

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

    This function will generate the following item 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 as well as defining a network of events that occur on the page.

    Function elAttr has the following type :

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

    It takes the name of the tag, the attributes, and the contents of the element.This function, and in general the entire set of functions that build DOM , what their internal widget returns.In this case, our elements are empty, so we use blank This is one of the most frequent uses of this function – when you want to make the bodyof an element blank.The function el Its inputparameters are just the name of the tag and its content, in other words it is a simplified version of the function elAttr without attributes.The other 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 so it is the text that is passed to it that will be output. In order to embed a piece of html, there is a function elDynHtml

    It must be said that in the above example the use of MonadWidget is redundant, because this part builds an immutable section of DOM And, as stated above, MonadWidget allows you to build or rebuild DOM , and also allows you to define a network of events. The functions used here require only the presence of the class DomBuilder , and here, indeed, we could only write this restriction. But in general, there are much more restrictions on the monad, which makes development difficult and slows down if we only write the classes we need now. That’s why there is a class MonadWidget which is a kind of Swiss knife. For those who are curious, here is a list of all classes, which 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, PerformEventt 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 of the page, but first let’s define the data type we’ll use to set the :

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

    The body will have the following structure :

    rootWidget :: MonadWidget t m => m ()rootWidget =divClass"container" $ doelClass"h2" "text-center mt-3" $ text "Todos"newTodoEv <- newTodoFormtodosDyn <- foldDyn(:)[] newTodoEvdelimitertodoListWidgettodosDyn

    Function elClass takes tag name, class(es), and contents as input. divClass is a shortened version of elClass "div"

    All the functions described above are just for visual presentation and do not carry any logic, unlike the function foldDyn It 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 (Dynamict b)

    It is similar to foldr :: (a -> b -> b) -> b -> [a] -> b and essentially performs the same role, only as a list here is an event. The resulting valueis wrapped in a container Dynamic because it will be updated after each event. The update process is defined with a parameter function that takes as inputthe value from the event that occurred and the currentvalue from Dynamic On their basis, a new value is formed, which will be in Dynamic This update will occur each time an event occurs.

    In our example, the function foldDyn will update the dynamic job list (initially empty) as soon as a new job is added from the inputform. New jobs are added to the beginning of the list because the function (:)

    Function newTodoForm builds that part of DOM which will contain the form for entering the job description, and returns the event which carries the new Todo It is when this event occurs that the job list will be updated.

    newTodoForm :: MonadWidget t m => m (Event t Todo)newTodoForm = rowWrapper $el "form" $divClass "input-group" $ doiEl <- inputElement$ def initialAttributes.~( "type" =: "text"<> "class" =: "form-control"<> "placeholder" =: "Todo" )letnewTodoDyn = newTodo <$> value iElbtnAttr = "class" =: "btn btn-outline-secondary"<> "type" =: "button"(btnEl, _) <- divClass "input-group-append" $elAttr'"button" btnAttr $ text "Add new entry"pure $ tagPromptlyDynnewTodoDyn $ domEventClickbtnEl

    The first innovation we see here is the function inputElement Its name says it all: it adds an element input As a parameter it takes the type InputElementConfig It has many fields and inherits several different classes, but in this example we are most interested in adding the necessary attributes to this tag, and this can be done by using the initialAttributes The function value is a method of the class HasValue and returns the value that is in the given input In the case of type InputElement it has the type Dynamict Text This value will be updated each time a change occurs in the input

    The next change we see here is the use of the function elAttr' The difference between functions with a stroke and functions without a stroke to build DOM is that these functions also return the element of the page itself, which we can manipulate in various ways. In our case, we need it so that we can get a click event on the element. This is what 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 item. In our case this is ()

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

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

    Its task is to put the value that is currently inside the Dynamic That is, the event that is the result of the function tagPromptlyDyn valDynbtnEv occurs at the same time as btnEv , but carries the value that was in valDyn For our example, this event will occur when the button is pressed and will carry the value from the text input field.

    It should be mentioned that functions which contain in their names the word , are potentially dangerous – they can cause loops in the event network. On the surface it will look as if the application hangs. The call tagPromplyDynvalDyn btnEv , if possible, should be replaced by tag (current valDyn) btnEv Function current gets Behavior from Dynamic These calls are not always interchangeable. If the update Dynamic and the event Event in tagPromplyDyn occur at the same moment, i.e. in the same frame, then the output event will contain the data received by Dynamic in that frame. In the case we are going to use tag (current valDyn) btnEv , 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 Behavior and Dynamic receive an update in the same frame, then Dynamic will be updated already in that frame, and Behavior will get a new value in the next frame. In other words, if the event occurred at time t1 and at the moment of time t2 , then Dynamic will have the value that brought the event t1 in the time interval [t1, t2] , and Behavior(t1, t2]

    Task function todoListWidget is to display the entire list of Todo

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

    This is where the function simpleList It has the following signature :

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

    This function is part of the package reflex , and in our case it is used to build repeating elements in DOM where these will be the consecutive elements of div It takes Dynamic list that can change over time, and a function to handle each item individually.In this case, it’s just a widget for displaying a single 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 is different from the function text takes as input a text wrapped in Dynamic In case the list item is changed, this value will also be updated in DOM

    There were also 2 functions used that were not mentioned : 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 arowWrapper ma =divClass "row justify-content-md-center" $divClass "col-6" ma

    Function delimiter simply adds a delimiter element.

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

    Creating a web application in Haskell using Reflex.Part 1

    You can see the result in our repository

    This is all it takes to build a simple and stripped down application Todo In this part we covered setting up the environment and got right down to developing the application itself. In the next part we will add working with a list item.

    You may also like :

    1. Creating a web application in Haskell using Reflex. Part 2
    2. Creating a web application in Haskell using Reflex. Part 3
    3. How we choose programming languages in Typeable
    4. Comparison of Elm and Reflex

    You may also like