Build a ReasonML Calculator App: Part I

Yes, yet another ReasonML tutorial!

Taha Ben Masaud
Sep 9, 2018 · 7 min read

Introduction

This tutorial aims to demonstrate how a simple calculator app could be built using ReasonML.

Detailing the advantages of using a statically typed functional programming language, such as ReasonML, is outside the scope of this tutorial. However, references to the merits of the language that aid developers with catching bugs and reason about their code more carefully will be briefly highlighted.

Calculator Specs

It is important to define the characteristics that will define the calculator app. It would be even better if those specs can be referenced from an existing implementation.

FreeCodeCamp’s calculator challenge offers a detailed set of specifications and also links an existing implementation. However, some of those requirements will be ignored in favour of simplicity. Specs related to definition of HTML tags and CSS classes will also be ignored.

The requirements can thus be summarised as follows:

  1. The calculator should perform formula logic (vs immediate execution logic.)
  2. The calculator should be able to handle decimal numbers.
  3. A decimal number can have only one dot (.)
  4. Operations entered consecutively will all be ignored but the last one.
  5. Operations (+-x/) should trigger a continuation of the formula logic if selected after pressing (=).
  6. Numbers should not start with multiple zeros.

Background

The ReasonML project was initiated by Facebook. Thus, it should not come as a surprise that support for React and JSX can be easily added to any ReasonML project. This fact coupled with React’s popularity makes using ReasonReact a reasonable choice as a view library to develop our calculator app.

It is also important to note that ReasonML/Ocaml projects are compiled to JavaScript using BuckleScript. This means that:

  1. One can choose to use either ReasonML or OCaml.
  2. In addition to package.json , ReasonML/OCaml dependencies are maintained in a separate file called bsconfig.json .
  3. The Babel and TypeScript compilers are not needed.

Because setting up the configuration files and other boilerplate code, such as webpack, is not directly relevant to the objective of this tutorial, it will not be presented here. However, ReasonML documentation offers a wealth of resources on how to bootstrap ReasonML projects.

Alternatively, the accompanying repository for this project can be cloned from here.


Section 1: Calculator Markup

Defining the calculator’s HTML definition is a good starting point. The complete definition can be inspected by checking out to the section-01 branch of the accompanying repository.

Before we start adding any code, let’s briefly inspect the project’s structure.

Application Structure

Ignoring boilerplate, the application structure will be as simple as defining three main files: App.re , App.css , and index.html .

The App.re file will host the logic that determines the updates to UI as well as the HTML definition of the app. The App.css file will define CSS styles while the index.html file will be the entry point for our calculator app.

ReasonML & JSX

As previously mentioned, ReasonML supports JSX. However, the JSX code needs to be wrapped around by a React component. ReasonReact offers two types of components: stateless, and reducer components. The former corresponds to stateless functional components, while the later corresponds to class-based components in ReactJS. Let’s start off by using a stateless component. In App.re , add the following:

let component = ReasonReact.statelessComponent("App");

Here, we bind a variable called component to a stateless component named "App" . Note that we didn’t have to explicitly import ReasonReact into this file.


Now, the HTML definitions can be added tocomponent.

The first line let toString = ReasonReact.string; defines a helper function that converts a string to a reactElement.

Next, we define the make variable that binds a function definition. It is important to name this variable make as it would the entry point for the BuckleScript compiler; hint: check the build script in package.json.

The function returned by make takes one optional argument that defines a list of ReasonReact components which in turn could be rendered as children of the current component. Our component does not render other components and hence the argument’s name starts with an underscore to indicate it is not going to be used.


A quick inspection of the make function reveals that it simply returns a JavaScript object. But this is not JavaScript! This data structure is called a record in ReasonML and the syntax used to define it is identical to that of JavaScript objects.

In addition to render, the record takes all the fields defined in component by means of destructuring: i.e. ...component,.

The render field defines a function that takes an optional argument referencing the make component and returns an HTML definition in the form of JSX. Note that the toString helper function had to be used with every string in order to convert it to HTML text. Note also the use of the pipe operator |>. It basically indicates that the function on its right operates on the result of the expression to its left. The beauty of the pipe operator is its ability to succinctly define a chain of consequent function calls:

1 |> addTwo |> multiplyBy10 |> subtractThree /* => 27 */

Therefore, "+" |> toString is equivalent to toString("+").

Now, run yarn build followed by yarn build:webpack and then load index.html in a browser that supports CSS-grid. A calculator that resembles our reference implementation should be visible on the page. However, the results section is static and does not appear to react to button clicks. In the next section, we’ll make the results area show the expression entered by the user.

Section 2: React to User Input

If you don’t want to manually copy and paste the changes that will be introduced in this section, you can checkout to the section-02 branch of the accompanying repository.

Reducer Component

The fact that our root component is stateless means that the app inherently cannot react to user input. Therefore, in order to be able to update the UI as a result of user actions, the stateless component must be transformed to a reducer component.

The word reducer will probably ring a bell for those familiar with Redux: the popular state management library for React. Indeed, Redux, and to some extent React, adopt and encourage functional programming principles such as immutable state and uni-directional flow of data. Because ReasonML is, by default, a functional language, those principles are available out of the box. For example, variables declared in ReasonML are immutable by default. I say by default because ReasonML (and hence OCaml) offer an escape hatch for those who want to use mutable variables.

A reducer component blends Redux and React and therefore, it does not have a state. Rather, it immutably updates a variable that holds the state of the program. To make it more confusing, this variable is called state and is initialised using a field called initialState. For those who have used old versions of ReactJS, prior to the wide spread adoption of ES6 classes, will remember the old ReactJS API where the initial state was specified in the getInitialState. Let’s have a look at upgrade to our root component:

Note that:

  1. There is a new function labelled reducer that determines state changes.
  2. Updating the state of a reducer component can be triggered by calling ReasonReact.Update in the reducer function.
  3. The render function takes the component’s state as an argument and thus any changes to this state will result in a UI re-render.
  4. The render function takes a second argument called send. This function is used to dispatch actions in response to user input. Note the similarities with Redux.
  5. The state is of a record type that has two fields: expression and result.

Further Notes

In addition to the above notes, one of the most important additions in the last snippet is the use of the type identifier. The BuckleScript compiler would throw an error if the expression type state = ... was not included. The compiler would say something like: the type of the initialState variable cannot be inferred.

That is, the compiler attempts to identify (or infer) the types of all variables based on what it sees on the right hand side of the = sign. In the case of initialState, its type does not correspond to any built-in types such as string or float. Therefore, we have to explicitly define the type of initialState to be of record type that has two fields: expression and result. This is similar to how state would be defined in TypeScript.

Exercise: remove the type definition of state and see how useful the BuckleScript compiler is.


Note also how strings can be concatenated in ReasonML:

state.expression ++ action

This is very different from OCaml’s ^ operator.

Another thing to note is string_of_float? Where did this come from?

string_of_float is a function that is part of the Pervasives module which is included in the standard library. As its name suggests, this functions coerces a float value into a string.

Now, go ahead and rebuild bundle.js. Load the app in a browser and see how the results section now shows the inputs entered by the user.

In the next part of this tutorial, we’ll use the type system to build a model that represents the mathematical expression stored in the calculator’s state. We’ll also exploit pattern matching to sanitise the model against the specs defined at the beginning of this tutorial.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade