Emulating Immediate Execution

All the prior articles on writing a calculator in GHCjs feature old school functionality, namely immediate execution, which is a terrible UX. If you ever make an actual calculator app, at least let your users type into an input the expression they want evaluated. However, this choice makes good sense, as it requires a state machine and some other properties that make it good for learning.

This portion of the tutorial makes use of Lens in an effort to show how you might leverage optics in your applications.

Digits

Let’s model out our calculator as types. We could easily just use Char or Int to represent our numbers, however this violates the principle of making illegal states unrepresentable. So instead we will use an ADT and marshal.

data Digit
  = Seven | Eight | Nine
  | Four  | Five  | Six
  | One   | Two   | Three
  | Zero deriving (Eq, Show, Ord, Enum, Bounded)

Next, before we can make buttons for the digits, we need a way marshal a Digit to its respective Char, which can be handled nicely by a Prism. We need it to be a Prism because not all Char s have a corresponding Digit.

charDigit :: Prism' Char Digit

Now that we can marshal between the Haskell representation and one we can show our user, we can make a button for digits:

digit :: Digit -> Html m Digit
digit d = button [ onClick d (1)
                 , className $ "d" <> d' (2)
                 ] [ text d' ]
  where d' = d ^. re charDigit . to (pack . pure)
1 When clicking the button, we send the corresponding digit
2 Add a class for styling later

Entries

Digit gives us everything we need to represent our number pad, but it is not enough to represent a user inputed number. So we can make a simple type to represent an entry consisting of multiple digits.

newtype Entry = Entry [Digit]

This works so long as our calculator need only work with Natural numbers, however we want to do a bit more. We want to allow for decimals as well as negative numbers.

Decimals

To allow for decimals, we need to have a distinction so when the user presses a digit on the number pad, we know whether to add it before or after the decimal. We also need to ensure that when the user presses . we can move from one state to the other.

data Entry = Whole [Digit] | [Digit] :<.> [Digit]

With this we can implement the decimal interaction logic easily. When the user presses . and we have a whole number, we transition to the :<.> constructor.

addDecimal :: Entry -> Entry
addDecimal = \case
  Whole xs -> xs :<.> []
  ys       -> ys

When the user presses a digit button and we have a whole number, we append the new digit to the end of the list. Otherwise, we append to the space after the decimal.

applyDigit :: Digit -> Entry -> Entry
applyDigit d = \case
  Whole xs   -> Whole $ xs <> [d]
  xs :<.> ys -> xs :<.> (ys <> [d])

Negative

To allow for negatives, we can just make the Entry type recursive.

data Entry = Whole [Digit]
           | [Digit] :<.> [Digit]
           | Negate Entry

By adding the Negate constructor we can represent negatives of any existing Entry in the system. This makes the logic for the -/+ button straight forward.

neg :: Entry -> Entry
neg = \case
  Negate e -> e
  e -> Negate e

Show

Now to show the entry to our user in the calculator readout, we need to convert to a human readable version.

instance Show Entry where
  show = let asChar = traverse . re charDigit in \case
    Whole xs   -> xs ^.. asChar
    xs :<.> ys -> xs ^.. asChar <> "." <> ys ^.. asChar
    Negate e   -> '-' : show e

This shows instance results in an intuitive display for the user where each button press is reflected 1:1. (IE no trailing ".0" or other issues)

Double

Lastly on the subject of entries, we need the ability to do math. To accomplish this we need to marshal entries to an actual number type (in this case Double), as well as marshal our results back to an Entry. We do this by creating an Iso.

frac :: Iso' Entry Double

This allows us to easily move in both directions.

Outlaw
The source version is not a lawful isomorphism, but this outlaw is harmless. Producing a lawful instance is left as an exercise for the reader.

Operators

We need a representation of operators to allow our user to perform work. We can do this with another simple enumeration:

data Operator
  = Addition
  | Multiplication
  | Subtraction
  | Division
  deriving (Eq, Enum, Bounded)

And as before we need to show this to our user:

instance Show Operator where
  show = \case
    Addition       -> "+"
    Subtraction    -> "−"
    Multiplication -> "×"
    Division       -> "÷"

operate :: Maybe Operator -> Operator -> Html m Operator
operate active o = button
  [ onClick o (1)
  , className ("active", Just o == active) (2)
  ] [ text . pack $ show o ]
1 When clicked, the button sends the corresponding operator
2 Set the "active" class if this button is the active button (for styling)

Model

Now we can actually define our model. Ultimately, the immediate execution calculator is a state machine with two major states:

  1. There is a current entry.

  2. There is a current entry, and a previous entry, and an operation.

Input Readout Current Operation Entry

[]

1

1

[1]

2

12

[1,2]

+

+

[]

+

[1,2]

4

+4

[4]

+

[1,2]

=

16

[1,6]

One way to model this is with the following type:

data Operation = Operation
  { _operator :: Operator
  , _previous :: Entry
  } deriving (Eq, Show)

makeFieldsNoPrefix ''Operation

data Model = Model
  { _current   :: Entry (1)
  , _operation :: Maybe Operation (2)
  } deriving (Eq, Show)

makeFieldsNoPrefix ''Model
1 We always have a current entry.
2 We might have a previous entry and an operation.

Buttons

Now let’s start building the final view. The calculator needs a readout area that shows the user the current state of the system.

readout :: Model -> Html m a

Because the readout consumes the state but never produces an update we should leave the HTML parametric.

The presence a in the above signature is proof that the HTML produced is non-interactive.

We also need our buttons. This calculator will have the following familiar buttons:

All Clear

Resets the calculator to the initial state.

clear :: Html m Model
clear  = button [ class' "clear", onClick initial ] [ "AC" ]

Negate

Negates the current entry. Phrased on the button as [-/+].

posNeg :: Model -> Html m Model
posNeg x = button [ class' "posNeg", onClick (x & current %~ neg) ] [ "-/+" ]

Numberpad

The nine digit pad (excluding 0).

numberpad :: Html m Digit
numberpad = H.div "numberpad"
  . L.intercalate [ br'_ ] (3)
  . L.chunksOf 3 (2)
  $ digit <$> [minBound .. pred maxBound] (1)
1 Get a list all members of our Digit type, excluding Zero. We are leveraging the derived Ord instance here, as the type definition already has the digits arranged for the number pad, with Zero as maxBound.
2 Split the resulting list of HTML into rows of three buttons each. (chunksOf is a part of Data.List)
3 Add <br/> between each row.

Decimal

A button to apply adding a decimal point to the current entry.

dot :: Model -> Html m Model
dot x = button [ onClick $ x & current %~ addDecimal ] [ "." ]

Arithmetic

Each operator button does the following: . Sets the operation to the given operator. . Sets the previous entry to be the current entry. . Blanks the current entry.

operations :: Model -> Html m Model
operations x = H.div "operate" $ fmap (\o -> x
  & operation .~ Just (Operation o (x ^. current))
  & current   .~ noEntry) (3)
  . operate (x ^? operation . traverse . operator) (2)
 <$> [minBound .. maxBound] (1)
1 Leverage Bounded and Enum to get a list of operators.
2 Get the current operator if there is one (for display purposes).
3 Apply the update described above.

Equals

Last we come to equals. This button should calculate the result of our operation, blank the operator and previous entry, and set the current entry to our result.

calcResult :: Model -> Model
calcResult x = x
  & operation .~ Nothing
  & current .~ case x ^. operation of
    Nothing -> x ^. current
    Just o ->
      let l = o ^. previous . frac
          r = x ^. current  . frac
      in (^. from frac) $ case o ^. operator of
      Addition       -> l + r
      Subtraction    -> l - r
      Multiplication -> l * r
      Division       -> if r == 0 then l else l / r

Based on the above examples, you should be able to see what is going on in this code. Writing a button to perform this operation is straightforward.

equals :: Model -> Html m Model
equals x = button [ class' "equals", onClick $ calcResult x ] [ "=" ]

The View

Now, we can construct the final view by composing together our existing parts.

view :: Model -> Html Model
view x = H.div "calculator"
  [ readout x
  , H.div "buttons"
    [ clear, posNeg x , operations x
    , putDigit <$> numberpad
    , H.div "zerodot"
      [ putDigit <$> digit Zero, dot x , equals x ]
    ]
  ] where putDigit d = x & current %~ applyDigit d

And we are done.

Conclusion

You can review the final code here, and see it running here.

Simplicity
There were no Monads, no message types, no FRP networking, no causality, and we never considered when or how components render. Instead we focused on data structures, and simple functions with simple types.

Thank you for your time.