loading...

I thought multiplication was going to be easy

dwayne profile image Dwayne Crooks ・2 min read

After adding addition and subtraction to my calculator I decided to proceed with multiplication.

Due to good separation of concerns it required little change to the code. However, that wasn't going to be the end of it.

After using the calculator for a while I stumbled upon the expression 1+2*3.

Incorrect answer due to ignoring operator precedence

1+2*3=1+6=7 but my calculator gave 9 because it did 1+2*3=3*3=9. I forgot that my expression evaluator didn't handle operator precedence.

A good opportunity to add a test

I knew improving my expression evaluator would require a non-trivial code change and I knew that I didn't want to be constantly entering expressions via the UI to check if I correctly implemented the evaluator.

So I added a failing test.

operatorPrecedenceSuite : Test
operatorPrecedenceSuite =
  describe "operator precedence" <|
    [ test "multiplication is done before addition" <|
        \_ ->
          let
            calculator =
              Calculator.new
                |> Calculator.process (Digit 1)
                |> Calculator.process (Operator Plus)
                |> Calculator.process (Digit 2)
                |> Calculator.process (Operator Times)
                |> Calculator.process (Digit 3)
                |> Calculator.process Equal
          in
            calculator
              |> Calculator.toDisplay
              |> Expect.equal { expr = "1+2*3=7", output = "7" }
]

The shunting yard algorithm

I just implemented enough of the shunting yard algorithm to get addition, subtraction and multiplication working correctly. With plans to add division in the near future.

Correct answer

Here's the code if you're interested in the implementation details.

The operatorPrecedenceSuite test passed and I was elated.

Elm handled the complexity well

Initially the expression evaluator was hidden inside the Calculator module. But the complexity of the evaluator got to the point where I wanted to ensure it was correct without having to go through the Calculator's public API.

To handle the complexity I factored the evaluator into its own module, Expr, with the following public API:

module Expr exposing (Expr(..), eval, toString)

This allowed me to extensively test it. For e.g. here's one of the tests I added:

test "1+2*3" <|
  \_ ->
    Expr.eval (Add (Const 1) (Mul (Const 2) (Const 3)))
      |> Expect.equal 7

You can view the full suite of tests here.

It was simple

Even though I ended up writing a lot of extra code I still felt that Elm managed the complexity well. I still understand how my code works, I know where I need to make changes when I need to add a new feature and the types along with the tests make me feel extremely confident in my implementation.

Though adding multiplication to the calculator wasn't easy. Elm made it simple.

Posted on Aug 7 '19 by:

dwayne profile

Dwayne Crooks

@dwayne

A full stack web developer who has an interest in programming language theory, interpreters, compilers and type theory. I enjoy programming with Elm and Haskell in my free time.

Discussion

markdown guide