Build a ReasonML Calculator App: Part II
Types, variants, and pattern matching

Section 3: Sanitise Expressions
All we did in the last section was to simply map user inputs to a visible string on the app’s result section. In this section, we’ll sanitise the the expression such that the rules defined in the calculator specs are satisfied.
Update Function
Let’s borrow some architectural concepts from Elm! This will help us navigating our code with ease. Let’s add an update function:
Note that the new state is now determined by the value returned by the update function.
The body of the update function might look alien to those not familiar with pattern matching. Essentially, pattern matching is similar to the switch statement in JavaScript except that the compiler will warn developers of any unconsidered cases of data types it knows how to construct. The power of pattern matching will become more apparent when we encounter variants types shortly.
In addition to the compiler’s help, note the conciseness of expressing the different possible values a variable can take. In fact, other functional programming languages (e.g. Elm) have even more concise syntax of expressing pattern matching. In ReasonML/OCaml, the vertical bar, |, is used to separate the different patterns and can be thought of as: this variable can be something or something else or something else …etc. For each case, an expression, that evaluates to the desired outcome, is returned.
Modelling Calculator’s State
Equipped with this basic understanding of pattern matching, how can we handle the different values the action variable can take? Looking at the possible values that can be sent from the view, an actioncan take these following values:
+ - x / = 0 1 2 3 4 5 6 7 8 9 ClearIn ReasonML, there are two options:
- Consider every case separately.
- Categorise these values into different groups.
The second option sounds more promising as it would save us a lot of repeated code. We can do this using variants.
Variants:
A variant is a data structure that is used to group closely related types. For example, numbers from 0 to 9 can be grouped in a variant type called digit:
type digit =
| One
| Two
| Three
...One, Two, and Three are basically tags that represents what a variable of type digit can evaluate to. They are also known as constructors and they are always capitalised. They also do not need to be declared before used to construct a type. Combined with pattern matching, variants can make the data flow through the calculator logic smooth and easy to understand. Let’s add other variant types:
Note that the symbol type groups the number and operation types and adds a new constructor called Dot. This shows that types can be used as constructors of other types. Consequently, this allows the symbol type to be destructured to the variants that construct the number and operation types such as One and Add. This will become clearer as soon as we start using pattern matching for desctructuring our types.
The state type definition has also changed where expression is now a list of symbols rather than a simple string. expression now holds the model of the calculator’s state.
Constructing Types:
In order to start reaping the benefits of using the types we have just defined, we need to ensure that the action sent by the send function in the view corresponds to the action type. We can build an action type using its constructors such as:
let actionType = Formulate(Digit(Seven))Exercise: what is the constructed type for Arithmetic(Multiply)?
Now, we can update our component JSX as follows:
Because state.expression is now a list of type symbol, we cannot simply use state.expression |> toString. We need to map symbols to strings and then concatenate them. We can employ pattern matching to do the mapping:
Every mapping function will always return the same type. The compiler would throw an error during compilation if two or more branches of the pattern match return different types; e.g.
switch digit {
| Zero => "0"
| One => 1Here, the first branch matches the Zero constructor and return a value of string type. However, the second branch would match the One constructor but returns a value of int type. This results in an error during compilation and the compiler would point out the type discrepancy between the returned values.
Sanitisation:
We can sanitise state.expression according to the specs:
There is a sizeable logic in this last snippet. Let’s go through it slowly.
The result of the update function depends on the action triggered by the user. Hence, we use pattern matching to destruct msg into all its constructs: Formulate, Calculate, and Clear. For example, in the case the user clicks on the AC button, a Clear action is fired and is captured by our update function. The pattern matching then returns the initial state; thus clearing the app’s state.
In case msg destructs to Formulate, we destruct Formulate further to get the new symbol to be added to the app’s expression list: Formulate(symbol). Note symbol is a type variable; i.e. it can be called anything we like. Moreover, the pattern match for Formulate returns a record of type state where the result part is simply copied over from the previous state. The expression part goes through scrutinisation steps depending on the constructs making up the symbol variable.
Let’s consider the case where symbol destructs to digit. We first create a variable called digit so that we can further destruct it to its constructs such as One and Nine.
In order to ensure number do not start with a leading zero, we have to consider the case where digit destructs to Zero. A function called willZeroBeLeading returns True if the symbol preceding the current Zero is an arithmetic operation. If the preceding symbol evaluates to other symbol constructs, it then returns False.
In case symbol destructs to Dot, we need to ensure that a dot has not preceded in the current number. For example, if the expression current state is [Nine, One, Dot, Seven], a Dot should not be allowed to follow the Nine. Otherwise, the expression ends up looking like .91.7. Note that new symbols prepend the expression list; this is mainly because it is easier to take the first element of a list when destructuring that list in pattern matching expressions.
The function that determines whether a dot has preceded in the currently constructed number is a recursive function: let rec hasDotPreceded. It has to be recursive so that we can traverse the expression list backwards until we hit another dot or an arithmetic expression. If it hits the former, the function returns True, while if it hits the later first, it returns False.
Now try to build the project and load index.html in the browser. Test whether the rules defined in Part I apply to the calculator’s expression.
In the next part of this tutorial, we’ll finish off by computing the result of evaluating the calculator’s expression.
