| title |
|---|
Calculator |
The Calculator.elm program implements a simple calculator that can be used to perform arithmetic operations. You can run it here. To use the calculator, click its buttons using your mouse.
The code is divided into three modules:
CalculatorModelCalculatorViewCalculator
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:
% CalculatorModel.elm module CalculatorModel where
import Char
import Maybe (withDefault)
import Result
import Set
import String
The following line defines a new data type called ButtonType:
% CalculatorModel.elm
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:
> Cons 1 (Cons 2 Nil)
Cons 1 (Cons 2 Nil) : ListOfInts
Union types may have type parameters. The following data type
represents lists of elements of an arbitrary type. The type of the
list elements is represeted by the a parameter:
type GenericList a = Nil | Cons a (GenericList a)
Here we create a list of characters containing the characters ‘a’, ‘b’ and ‘c’:
> Cons 'a' (Cons 'b' (Cons 'c' Nil))
Cons 'a' (Cons 'b' (Cons 'c' Nil)) : GenericList 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:
% CalculatorModel.elm
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 the 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 CalculatorView (..)
> 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 length of a
GenericList:
listSize : GenericList a -> Int
listSize lst =
case lst of
Nil -> 0
Cons _ tail -> 1 + listSize tail
If the lst list is empty, the first pattern matches, and the
function returns 0. The second pattern is more interesting. It
consists of the name of the type constructor (Cons) followed by the
_ character and the tail symbol. The _ character matches any
value, and it is used when we are not interested in the value being
matched. The tail symbol is a variable, that will acquire the value
of the second parameter of the Cons value. So, for example, if lst
is (Cons 4 (Cons 6 Nil)), then tail will have the value of Cons 6 Nil assigned to it. When the second pattern is matched, the function
returns 1 plus the result of a recursive call to itself with the
tail value as argument.
> listSize Nil
0 : Int
> listSize (Cons 4 (Cons 6 Nil))
2 : Int
The CalculatorModel module defines a record type representing the
calculator state.
% CalculatorModel.elm
type alias CalculatorState = {
input: String,
operator: String,
number: Float
}
The calculator needs to remember three things, represented by three state members:
inputis the number that the user enters into the calculator by clicking on the number buttons and the dot buttonoperatoris one of the four arithmetic operations selected by the usernumberis 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 and calculates the new state.
% CalculatorModel.elm
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.
% CalculatorModel.elm
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:
% CalculatorModel.elm
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
numbermember is calculated by thecalculatefunction based on the old state - the value of the operator clicked by the user is stored in the
operatormember - the
inputis reset to an empty string
The calculate function is defined as follows:
% CalculatorModel.elm
calculate : Float -> String -> String -> Float
calculate number op input =
let number2 = withDefault 0.0 (Result.toMaybe (String.toFloat input))
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:
> String.toFloat
<function: toFloat> : String -> Maybe Float
The return value is of type Maybe Float. We have already met Maybe
in one of earlier chapters. Maybe is a union type defined in the
Maybe module as follows:
type Maybe a = Just a | Nothing
Thus the String.toFloat function may return one of two values: Just Float or Nothing. The first one is returned if the conversion
succeeds, the second one otherwise. The calculate function could
pattern match on the result using a case expression, but it uses the
the withDefault function instead, to retrieve the result, with the
0.0 value 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 mamber, 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.
% CalculatorModel.elm
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:
% CalculatorView.elm module CalculatorView where
import CalculatorModel (..)
import Color (rgb)
import Graphics.Collage (LineCap(Padded), collage, defaultLine, filled, outlined, rect, toForm)
import Graphics.Element (Element, container, down, flow, layers, midRight, middle, right, spacer)
import Graphics.Input (clickable)
import Signal (Signal, channel, send, subscribe)
import Text
After the imports, the makeButton function is defined. That function
creates an element representing a calculator button. It takes a string
that will be the button label, and a ButtonType value as arguments.
% CalculatorView.elm
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 |> Text.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:
colorof typeColor— represents the line colorwidthof typeFloat— represents the line width in pixelscapof typeLineCap— represents the shape of line endsjoinof typeLineJoin— represents the shape of line joinsdashingof type[Int}— represents the dashing patterndashOffsetof typeInt— represents the dashing offset
You do not have to construct the whole record yourself. The
defaultLine function returns a default line style. You can use it
and modify certain members. For example, to have a default line, but
with the width set to 5, you 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.elm module CalculatorViewTest1 where
import CalculatorModel (..)
import CalculatorView (..)
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:
% CalculatorView.elm
makeButtonAndSignal : String -> ButtonType -> (Element, Signal String)
makeButtonAndSignal label btnSize =
let button = makeButton label btnSize
buttonChannel = channel ""
message = send buttonChannel label
clickableButton = clickable message button
in
(clickableButton, subscribe buttonChannel)
To create a clickable element, we first need a channel. The
Channel type is defined in the Signal module and it represents a
place where messages can be sent to. You can also subscribe to a
channel, getting a signal. To create a channel, we use the channel
function, providing the default value of that channel’s signal as its
argument:
channel : a -> Channel a
In our function we use a String value as the argument to the
channel function. Thus the buttonChannel value has the Channel String type.
To create a message, that can be sent to the channel, we use the
send function. We need to give it two arguments: the channel and a
value to be sent through it.
send : Channel 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 channel, which is
obtained using the subscribe function.
subscribe : Channel a -> Signal a
Next, we use the makeButtonAndSignal function to create all the
calculator buttons and the associated signals.
% CalculatorView.elm
(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.
% CalculatorView.elm
display : CalculatorState -> Element
display state =
collage 240 60 [
outlined { defaultLine | width <- 2, cap <- Padded } <| rect 232 50,
toForm (container 220 50 midRight (Text.plainText <| showState state))
]
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.
% CalculatorView.elm
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.
% CalculatorView.elm
main = view initialState (600,600)
You can see it in action here.
The Calculator module is the main module of our calculator program:
% Calculator.elm module Calculator where
import CalculatorModel (..)
import CalculatorView (..)
import Signal (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.

