Building a reactive calculator with Keera Hails (5/5)
In the last few blog posts, we examined how to build a functional calculator web-app using reactive programming. We used the reactive programming library Keera Hails, but the ideas apply to other frameworks as well.
In this last blog post we are going to take our simple calculator app and turn it into a beautiful, professionally looking application. We will modify both the user interface and the internals, to organize it so that we can apply modifications easily in the future.
This is the last post in this series. At the bottom of the blog post, you can find a link to a live demo, a video, the source code, and all the blog posts in this series.
Improving the User Interface
Our app so far is functional, but it does not say “professional”. We don’t want to build examples that people run because they are written in Haskell. We should strive to build applications that people run because they are useful and beautiful. So let’s improve the GUI of our app.
If you recall, this is the state of the calculator at the end of the last blog post:
This application is nice, but it is clearly just for demonstration purposes. It’s not just the lack of functions in the calculator: the fonts in a real calculator look different, and the demo opens alone on a blank page. A real would have a more polished look. Addressing those points will require mainly CSS changes.
For our example, we want a font that looks more like a calculator. After searching for different LED-tagged fonts, we have selected Liquid Crystal, which has a more calculator-like appearance:
To add this font to our page, all we need to do is adapt the CSS to set the font face. Normally, adding fonts that are available on a website is simple: just define a font face pointing to the remote URL. In our application, we can do so by adding the new font face at the beginning of the style section in the head.html file.
@font-face {
font-family: LiquidCrystal;
src: url(LiquidCrystal-Normal.otf);
}
Here, the text inside url points to the file path where the font is. Normally, this would be a location, absolute (i.e., “/fonts/…”) or relative to where the page is deployed, that contains the file.
Later, all we need to do is use the font in the rules for .textview, which control the appearance of the calculator’s text box in the same head.html file. We also add a rule that says that the text must be aligned to the right. Here, we add it as a separate section, but you can just add it together with the existing rules for the same class.
.textview {
font-family: LiquidCrystal;
text-align: right;
}
For local testing, however, browsers can sometimes make it hard to test websites using resources that are outside the paths where the page is located. A possible workaround is to copy the file ‘LiquidCrystal-Normal.otf’ to the directory where the page is being installed by cabal (in our case, .cabal-sandbox/bin/keera-hails-demos-small.jsexe/. Alternatively, you can just install the font in your machine and use the following rule instead:
.textview {
font-family: 'Liquid Crystal', LiquidCrystal;
text-align: right;
}
Either way, the font is now rendered correctly and looks more the way that we desire:
Finally, to top our app we are going to add a nice-looking background. Our goal in this case is to place it in an environment that looks more like a real desk, and less like a digital product, even if it is running on a website. We choose the following background, which combines both old-style and modern elements, and has plenty of space on the desk to place the calculator:
To add this image, we modify the CSS rules for the page itself, that is, for the HTML element. We also modify the rules for the calculator to place it in the space available in the bottom right, and add a slight tilt to bring it into balance with the rest of the elements in the image:
html {
width: 100%;
height: 100%;
background-image: url('https://c.pxhere.com/photos/b2/22/coffee_cup_desk_keyboard_money_pencil_table_wallet-1522491.jpg!d');
background-size: cover;
background-position: center;
}
#calculator {
left: 55%;
bottom: 10%;
position: absolute;
transform: translate(-50%, -50%);
transform: rotate(12deg);
border-radius: 15px;
padding: 5px 5px;
background: #594F4F;
}
If all goes well, you should see something like the following:
Improving the code structure
In the long run, code structure is as important as the GUI, if not more. Poor code structure tends to reflect much later in the process, and it manifests as difficulty locating elements, difficulty debugging, and having to re-write large portions of the application every time we want to add new features, all of which are time consuming and expensive.
In an application like this, which is small, we suggest the following structure, which seeks to separate the conceptual type definitions, the model, the view, and the controller / reactive rules:
Main.hs
Controller.hs
Data/Calculator.hs
Data/Action.hs
Model.hs
View.hs
View/Types.hs
View/HTML.hs
The code of some of those modules is self-explanatory, and you can see the details in this repository. The code of the main module has now been split into four parts: abstract data types (Data/), a reactive model (Model.hs), a reactive view (View.hs and View/), and a set of reactive rules (Controller.hs).
Cross-platform Reactive View
First, we are going to create a datatype to represent the GUI as a collection of reactive values. You may want to build a GUI using types that are specific to the backend of your application (e.g., HTML Elements, GTK+ Widgets). However, creating an abstract layer makes it trivial to later use a different backend or even support several backends at the same time. The core type is defined in View/Types.hs:
data UI = UI
{ numbers :: [ReactiveFieldRead IO Int] -- 0 to 9
, operators :: [ReactiveFieldRead IO (Int -> Int -> Int)] -- plus, minus, times, div
, actions :: [ReactiveFieldRead IO Action] -- clear, equals
, textField :: ReactiveFieldReadWrite IO String
}
Now, all HTML-specific code is located in one module only, View/HTML.hs. This code is similar to the beginning of the main we had before, except that, now, the main buildUI operation, which creates the user interface, returns a value of type UI instead of being just a monadic action:
buildUI :: IO UI
buildUI = do
-- ... same code as before ...
return $ UI nums operators actions inputFieldText
The main View module simply imports and re-exports both modules. It would be trivial to adapt View.hs to import and re-export a different module, implementing a view for a different backend, depending on the compiler being used (GHC, GHCJS), the target architecture, or cabal flags.
Reactive Model
In our initial implementation, we had enclosed the abstract calculator in an MVar with callbacks (CBMVar), and lifted operations from the calculator abstract data type directly in the reactive relations.
As models become bigger, it starts becoming more important to provide a more fine-grained notion of change propagation inside the model itself: if you have thousands of widgets connected to different parts of the model, you do not want to update all of them whenever any part of the model changes.
A way to facilitate the growth of a project in that direction, a strong recommendation is to enclose the model in a reactive layer. That way, the reactive model becomes just a collection of reactive end-points, and the reactive controller becomes just a connection of reactive relations. Notice the parallelism between the reactive model’s API and the Calculator datatype’s API:
data Model = Model
{ modelAddDigit :: ReactiveFieldWrite IO Int
, modelApplyOperator :: ReactiveFieldWrite IO (Int -> Int -> Int)
, modelApplyAction :: ReactiveFieldWrite IO Action
, modelValue :: ReactiveFieldRead IO Int
}
reactiveModel :: IO Model
reactiveModel = do
modelCB <- newCBMVar mkCalculator
let model = cbmvarReactiveRW modelCB
let addDig = addDigit `modRW` model
appOp = applyOperator `modRW` model
appAct = applyAction `modRW` model
val = currentValue <^> model
return $ Model addDig appOp appAct val
With these changes, the reactive relations become slightly simpler and all logic is transported to the model and the view. Remember that all this code does is connect reactive values through a series of reactive relations so that, when a reactive value changes, other reactive values are updated.
In this small project, we list all reactive relations in the module Controller.hs:
controller :: UI -> Model -> IO ()
controller ui model = do
textField ui <:= (show <^> modelValue model)
forM_ (numbers ui) $ \button ->
button =:> modelAddDigit model
forM_ (operators ui) $ \button ->
button =:> modelApplyOperator model
forM_ (actions ui) $ \button ->
button =:> modelApplyAction model
In large projects, we recommend that you split that module into several submodules, grouped either by UI part or by model part, and make the Controller module just call each sub-controller’s reactive relation installation actions.
With these changes, our top-level application now becomes the ideal representation of an MVC or reactive-based definition:
import Controller (controller)
import Model (reactiveModel)
import View (buildUI)
main :: IO ()
main = do
ui <- buildUI
model <- reactiveModel
controller ui model
Other changes you should apply
The project we have created in these examples shows that, through careful design and by using the right abstractions, we can build Haskell applications that are beautiful on the inside and on the outside. It is now your turn to take this to the next level.
A real calculator would have many more features, like the soviet Elektronika MK-61, an advanced programmable calculator. Haskell is perfect to write compilers and interpreters, so you may be able to complete a full interpreter that way.
You may also want to make this application cross-platform. We promised that such a change would require changing one module only. To make this application work, for example, on desktop using GTK, all you need to add is a new View/GTK.hs module that provides a buildUI operation returning a UI containing reactive values, conditionally export that module in View depending on the user selection. Due to a particularity of how GTK works, you will also need to show the window and start the GUI thread after you install the conditions in the main function, for which we recommend you define a new operation startUI that all backends publish but only does something meaningful for GTK and backends that control the UI thread.
Your job as a professional Haskell developer does not stop here. This is just the beginning. If you want to create a true, professional application that you can deploy to the world, even if it is open source, you’d also want to take care of copyright notices in all files, find a proper license based on the libraries and assets you actually use, provide tests for your data structures and your user interface, lint your code, document all your definitions (e.g., using haddock), write a proper installation manual, write a good CI infrastructure that tests and deploys your code, etc. You should also expect to have to adapt the UI to be responsive and visible from any device, update and maintain your code regularly, and to beta-test your code with users to give your application that final touch that makes it unique.
Here’s a list of all resources available as part of this series, in case you want to go through it again:
-
Source Code: https://github.com/keera-studios/keera-hails/tree/develop/demos/keera-hails-demos-small (Note: the git history in this folder will allow you to see the incremental changes as they were being explained along the tutorial).
-
Tutorial series:
Call for contributions
We are currently looking for contributions to our technical blog. If you would like to write a follow-up blog post discussing how to implement a GTK or WX view, how to add new features, how to incorporate functions based on websockets and communications, how to deploy your web app automatically to a server using Travis or Github actions, or any other topic related to Haskell programming and interactivity, contact us at keera@keera.co.uk with your idea and we will get back to you.