Chapter 9. Calculator
The Calculator.elm program implements a simple calculator that can be used to perform arithmetic operations. You can run it here: Calculator.html. To use the calculator, click its buttons using your mouse.
The code is divided into three modules:
- CalculatorModel
- CalculatorView
- Calculator
We start our analysis with the CalculatorModel module defined in the CalculatorModel.elm file. The module starts with the declaration and a list of imports:
File CalculatorModel.elm (fragment): module CalculatorModel where import Char import Result import Set import String
The following line defines a new data type called ButtonType:
File CalculatorModel.elm (fragment): type ButtonType = Regular | Large
The definition starts with the type keyword followed by the type name, the equals sign and the type definition. The type keyword is used for defining so called ⒤union types⒰. Such types consist of a number of alternatives which are separated with the | character. In our case, we have two alternatives: Regular and Large.
Our data type is very simple. However, using the type keyword, it is possible to define more complex data types as well. For example, the following data type represents a list of integers:
type ListOfInts = Nil | Cons Int ListOfInts
The alternatives are sometimes called type constructors. Our ListOfInts data type defines two of them. The first one is called Nil and represents the empty list. The other one is more interesting. Its name is Cons and it has two arguments, which are actually type names. The first one is Int and the second one is ListOfTypes, which is the name of the type being defined! This means that we have a recursive definition here. What this definition tells us, is that a list is either and empty list (Nil) or a non-empty list (Cons) consisting of an Int value and another list.
As an example, let us create a two-element list, containing the values 1 and 2:
> type ListOfInts = Nil | Cons Int ListOfInts > Cons 1 (Cons 2 Nil) Cons 1 (Cons 2 Nil) : Repl.ListOfInts
Union types may have type parameters. The following data type represents trees which hold elements of an arbitrary type in the nodes. The type of the list elements is represented by the a parameter:
type Tree a = Leaf | Node a (Tree a) (Tree a)
Here we create a tree of characters containing the characters ‘a’, ‘b’ and ‘c’:
> type Tree a = Leaf | Node a (Tree a) (Tree a) > Node 'a' (Node 'b' Leaf Leaf) (Node 'c' Leaf Leaf) Node 'a' (Node 'b' Leaf Leaf) (Node 'c' Leaf Leaf) : Repl.Tree Char
Let’s now go back to the CalculatorModel module. The buttonSize function accepts a value of type ButtonType as argument and returns an integer number.
File CalculatorModel.elm (fragment): buttonSize : ButtonType -> Int buttonSize size = case size of Regular -> 60 Large -> 120
We use here the case expression, which let us pattern match on the individual type constructors (or, more generally, on patterns). Elm tries to match the value placed between the case and of keywords (size in our case) against the patterns defined after the of keyword. Each pattern is followed by the -> arrow and an expression which becomes the result of the whole case expression if the corresponding pattern is matched. The patterns are tried one by one, and once any of them matches, the others are skipped.
> import CalculatorModel exposing (..) > buttonSize Regular 60 : Int > buttonSize Large 120 : Int
Since the type constructors of the ButtonType type are very simple, the patterns used in the buttonSize function are also simple — they exactly correspond to the type constructors. As another example, let us analyze the following function, which calculates the height of a Tree. Notice, that we are defining that function in the repl, and since the definition spans several lines, we end the lines with the \ character to inform the repl that it should not try to evaluate yet the text that we have entered so far, and instead, it should allow us to continue in the next line. In other words, the \ characters are not part of the function definition, they are just needed because of making a multi-line definition in the repl.
> treeHeight tree = \ | case tree of \ | Leaf -> 0 \ | Node _ left right -> \ | 1 + max (treeHeight left) (treeHeight right) <function> : Repl.Tree a -> number
If the tree is a Leaf, the first pattern matches, and the function returns 0. The second pattern is more interesting. It consists of the name of the type constructor Node followed by the _ character and the left and right variables. The _ character matches any value, and it is used when we are not interested in the value being matched. The left and right variables will acquire the values of the second and third parameter of the Node value. So, for example, if tree is Node 2 Leaf (Node 3 Leaf Leaf), then left will get the value Leaf and right will get the value Node 3 Leaf Leaf. When the second pattern is matched, the function returns 1 plus the greater value of the results of a recursive calls to itself with the left and right values as the arguments.
> treeHeight Leaf 0 : number > treeHeight <| Node 'a' (Node 'b' Leaf Leaf) Leaf 2 : number
The CalculatorModel module defines a record type representing the calculator state.
File CalculatorModel.elm (fragment): type alias CalculatorState = { input: String, operator: String, number: Float }
The calculator needs to remember three things, represented by three state members:
- input is the number that the user enters into the calculator by clicking on the number buttons and the dot button
- operator is one of the four arithmetic operations selected by the user
- number is the result of previous computations (or zero at the beginning)
The exact rules of how the calculator works are implemented in the step function, which takes as arguments the current calculator state and the button clicked by the user (represented as a String) and calculates the new state.
File CalculatorModel.elm (fragment): step : String -> CalculatorState -> CalculatorState step btn state = if | btn == "C" -> initialState | btn == "CE" -> { state | input <- "0" } | state.input == "" && isOper btn -> { state | operator <- btn } | isOper btn -> { number = calculate state.number state.operator state.input, operator = btn, input = "" } | otherwise -> { state | input <- if | (state.input == "" || state.input == "0") && btn == "." -> "0." | state.input == "" || state.input == "0" -> btn | String.length state.input >= 18 -> state.input | btn == "." && String.any (\c -> c == '.') state.input -> state.input | otherwise -> state.input ++ btn }
The step function uses an alternative form of the if expression. The if keyword is followed by a number of conditions and expressions. Each condition is preceded by the | character. After each condition there is an arrow -> followed by an expression. The if expression verifies each condition, one by one, until the first one that evaluates to True. The expression that follows that condition becomes the result of the whole if expression. The last condition in our if expressions is otherwise, which evaluates to True, thus making that condition the “catch all” clause.
The following two forms of the if expression are thus equivalent:
if <condition> then <expression1> else <expression2> if | <condition> -> <expression1> | otherwise -> <expression2>
The step function works as follows. If the user selects the C button, the initial state, calculated by the initialState function, is returned.
File CalculatorModel.elm (fragment): initialState = { number = 0.0, input = "", operator = "" }
If the user selects the CE button, then input is set to zero, and the previously entered input is forgotten. If the user selects one of the operators, as verified by the isOper function, and if there was no previous input (the input is equal to an empty string), then the operator is saved in the new state. The syntax for updating the operator member looks as follows:
{ state | operator <- btn }
The state represents the old state. The operator is the name of the member being updated. The btn is the new value to be assigned to the operator member. The whole expression does not change the state value, but it returns a new value, similar to state but with the operator member updated. The isOper function is defined as follows:
File CalculatorModel.elm (fragment): isOper : String -> Bool isOper btn = Set.member btn (Set.fromList ["+","-","*","/","="])
The function uses two functions from the Set module. The Set.fromList function creates a set from a list. The Set.member function verifies if its first argument belongs to the set represented by the second argument.
If the user selects one of the operators, but there is already an input value present in the input field, then a whole new state is calculated and returned as follows:
- the value of the number member is calculated by the calculate function based on the old state
- the value of the operator clicked by the user is stored in the operator member
- the input is reset to an empty string
The calculate function is defined as follows:
File CalculatorModel.elm (fragment): calculate : Float -> String -> String -> Float calculate number op input = let number2 = case String.toFloat input of Ok n -> n Err _ -> 0.0 in if | op == "+" -> number + number2 | op == "-" -> number - number2 | op == "*" -> number * number2 | op == "/" -> number / number2 | otherwise -> number2
It first converts the value of the input member to a floating point number using the String.toFloat function. That function does not return a Float value however, as showed by the repl:
> import String > String.toFloat <function: toFloat> : String -> Result.Result String Float
The return value is of type Result String Float. Result is a union type defined in the Result module as follows:
type Result error value = Ok value | Err error
Thus the String.toFloat function may return one of two values: Ok Float or Err String. The first one is returned if the conversion succeeds, the second one otherwise. The calculate function pattern matches on the result using a case expression. The 0.0 value is used as a fallback in case the conversion fails.
After converting the input value to Float, the calculate function performs the appropriate (based on the value of the operator member) arithmetic operation on the value of the number member, and the result of converting the input to Float.
Finally (going back to the step function), if the user selects something else, which must be either a digit or a dot, then the input member is updated as follows:
- if the current input is empty or “0” and the dot is selected, the input is set to be “0.”
- if the current input is empty or “0”, the input is set to be equal to the label of the selected button
- if the current input has length equal or greater than 18, no new data is appended to the input
- if the current input contains a dot already, and the dot is selected, the input string remains unchanged
- otherwise, the label of the selected button is appended to the input string
There is one more function in the CalculatorModel module. The showState function converts the state to a string to be shown in the calculator display. The result is the value of the input member, unless it is empty, in which case the value of the number member is converted to a string and returned.
File CalculatorModel.elm (fragment): showState : CalculatorState -> String showState {number,input} = if input == "" then toString number else input
We can now turn our analysis to the CalculatorView module, which is defined in the CalculatorView.elm file. Its definition starts as follows:
File CalculatorView.elm (fragment): module CalculatorView where import CalculatorModel exposing (..) import Color exposing (rgb) import Graphics.Collage exposing (LineCap(Padded), collage, defaultLine, filled, outlined, rect, toForm) import Graphics.Element exposing (Element, centered, container, down, flow, leftAligned, layers, midRight, middle, right, spacer) import Graphics.Input exposing (clickable) import Signal exposing (Signal, mailbox, message) import Text
The first function to be defined in the module is makeButton. It creates an element representing a calculator button. It takes a string that will be the button label, and a ButtonType value as arguments.
File CalculatorView.elm (fragment): makeButton : String -> ButtonType -> Element makeButton label size = let xSize = buttonSize size buttonColor = rgb 199 235 243 in collage xSize 60 [ filled buttonColor <| rect (toFloat (xSize-8)) 52, outlined { defaultLine | width <- 2, cap <- Padded } <| rect (toFloat (xSize-8)) 52, Text.fromString label |> Text.height 30 |> Text.bold |> centered |> toForm ]
A button is composed of a filled rectangle, which forms the button background color, an outlined rectangle forming the button border, and a text. The buttonSize function from the CalculatorModel module is used for calculating the horizontal size of the button. The auxiliary buttonColor function returns the button color.
The outlined function expects in its first argument a value of type LineStyle, which is a record type defined in the Graphics.Collage module. The record contains the following members:
- color of type Color — represents the line color
- width of type Float — represents the line width in pixels
- cap of type LineCap — represents the shape of line ends
- join of type LineJoin — represents the shape of line joins
- dashing of type [Int} — represents the dashing pattern
- dashOffset of type Int — represents the dashing offset
We do not have to construct the whole record ourselves. The defaultLine function returns a default line style. We can use it and modify certain members. For example, to have a default line, but with the width set to 5, we can use the expression:
{ defaultLine | width <- 5 }
The cap member can be set to values Flat (default), Round or Padded. The join member can be set to Smooth, Clipped or Sharp Float (Sharp 10 is the default). The following figure illustrates the various line caps and joins:
The first one has the cap set to Flat and the join set to Sharp 10. The second one has the cap set to Flat and the join set to Smooth. The last one has the cap set to Padded and the join set to Clipped. The red dots indicate the position of one of joins and one of caps.
The following figure illustrates the dashing. It presents three lines. The first one has dashing set to [] (the default). The second, to [40,10] and the third one to [40,10,40].
The CalculatorViewTest1.elm program (showed below) can be used to visually test the makeButton function (try it here: CalculatorViewTest1.html).
File CalculatorViewTest1.elm: module CalculatorViewTest1 where import CalculatorModel exposing (..) import CalculatorView exposing (..) main = makeButton "test" Large
Being able to create a button is not enough for our purposes. What we need is a clickable button. A button, which will have some kind of signal associated with it. We create such buttons using the makeButtonAndSignal function:
File CalculatorView.elm: makeButtonAndSignal : String -> ButtonType -> (Element, Signal String) makeButtonAndSignal label btnSize = let button = makeButton label btnSize buttonMailbox = mailbox "" buttonMessage = message buttonMailbox.address label clickableButton = clickable buttonMessage button in (clickableButton, buttonMailbox.signal)
To create a clickable element, we first need a mailbox. The Mailbox type is defined in the Signal module and it represents a place where messages can be sent to. It also has a signal associated with it. To create a mailbox, we use the mailbox function, providing the default value of that mailbox’s signal as its argument:
mailbox : a -> Mailbox a
In our function we use a String value as the argument to the mailbox function. Thus the buttonMailbox value has the Mailbox String type.
To create a message, that can be sent to the mailbox, we use the message function. We need to give it two arguments: the mailbox and a value to be sent through it.
message : Mailbox a -> a -> Message
We can now use the clickable function to turn a regular button into a clickable one. The clickable function takes a message and an element, and returns a clickable version of that element.
clickable : Message -> Element -> Element
Our makeButtonAndSignal function returns a pair of values: the clickable button and the signal associated with the mailbox. Next, we use the makeButtonAndSignal function to create all the calculator buttons and the associated signals.
File CalculatorView.elm (fragment): (button0, button0Signal) = makeButtonAndSignal "0" Regular (button1, button1Signal) = makeButtonAndSignal "1" Regular (button2, button2Signal) = makeButtonAndSignal "2" Regular (button3, button3Signal) = makeButtonAndSignal "3" Regular (button4, button4Signal) = makeButtonAndSignal "4" Regular (button5, button5Signal) = makeButtonAndSignal "5" Regular (button6, button6Signal) = makeButtonAndSignal "6" Regular (button7, button7Signal) = makeButtonAndSignal "7" Regular (button8, button8Signal) = makeButtonAndSignal "8" Regular (button9, button9Signal) = makeButtonAndSignal "9" Regular (buttonEq, buttonEqSignal) = makeButtonAndSignal "=" Regular (buttonPlus, buttonPlusSignal) = makeButtonAndSignal "+" Regular (buttonMinus, buttonMinusSignal) = makeButtonAndSignal "-" Regular (buttonDiv, buttonDivSignal) = makeButtonAndSignal "/" Regular (buttonMult, buttonMultSignal) = makeButtonAndSignal "*" Regular (buttonDot, buttonDotSignal) = makeButtonAndSignal "." Regular (buttonC, buttonCSignal) = makeButtonAndSignal "C" Large (buttonCE, buttonCESignal) = makeButtonAndSignal "CE" Large
Besides the buttons, the calculator needs a display where the results of the calculation as well as the user input will be shown. The display function creates it.
File CalculatorView.elm (fragment): display : CalculatorState -> Element display state = collage 240 60 [ outlined { defaultLine | width <- 2, cap <- Padded } <| rect 232 50, toForm (container 220 50 midRight (showState state |> Text.fromString |> leftAligned)) ]
It takes the calculator state as argument and uses the showState function to present it to the user. Finally, the view function combines the components and draws the calculator.
File CalculatorView.elm (fragment): view : CalculatorState -> (Int, Int) -> Element view value (w, h) = container w h middle <| layers [ collage 250 370 [ rect 248 368 |> outlined { defaultLine | width <- 3 , cap <- Padded } ], flow down [ spacer 250 5, flow right [ spacer 5 60, display value ], flow right [ spacer 5 60, buttonCE, buttonC ], flow right [ spacer 5 60, buttonPlus, button1 , button2, button3 ], flow right [ spacer 5 60, buttonMinus, button4 , button5, button6 ], flow right [ spacer 5 60, buttonMult, button7 , button8, button9 ], flow right [ spacer 5 60, buttonDiv, button0 , buttonDot, buttonEq ] ] ]
The view function takes two arguments: the calculator state, and a pair representing the window sizes. The CalculatorView module defines a main method for testing purposes.
File CalculatorView.elm (fragment): main = view initialState (600,600)
You can see it in action here: CalculatorView.html.
The Calculator module is the main module of our calculator program.
File Calculator.elm: module Calculator where import CalculatorModel exposing (..) import CalculatorView exposing (..) import Signal exposing (foldp, map2, mergeMany) import Window lastButtonClicked = mergeMany [ button0Signal, button1Signal, button2Signal, button3Signal, button4Signal, button5Signal, button6Signal, button7Signal, button8Signal, button9Signal, buttonEqSignal, buttonPlusSignal, buttonMinusSignal, buttonDivSignal, buttonMultSignal, buttonDotSignal, buttonCSignal, buttonCESignal ] stateSignal = foldp step initialState lastButtonClicked main = map2 view stateSignal Window.dimensions
The lastButtonClicked function combines individual signals associated with the calculator buttons into one signal using the mergeMany function from the Signal standard library module.
mergeMany : List (Signal a) -> Signal a
As the signature shows, all the signals in the input list need to have the same type.
The stateSignal function uses the foldp function to combine the lastButtonClicked signal with the step function from the CalculatorModel module.
Finally, the main function combines the stateSignal and Window.dimensions signals with the view function from the CalculatorView module.
So far, we have only used the mouse to interact with our programs. In the next chapter we will learn how to use keyboard releated signals.