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:
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:
% 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:
input
is the number that the user enters into the calculator by clicking on the number buttons and the dot buttonoperator
is one of the four arithmetic operations selected by the usernumber
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 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
number
member is calculated by thecalculate
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:
% 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:
color
of typeColor
— represents the line colorwidth
of typeFloat
— represents the line width in pixelscap
of typeLineCap
— represents the shape of line endsjoin
of typeLineJoin
— represents the shape of line joinsdashing
of type[Int}
— represents the dashing patterndashOffset
of 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.