Codea Calculator
Codea Calculator
Warning
This is long. Bring food and water, or read it in parts.
Be aware of this, please: with all these words, all this explanation, the project itself goes very smoothly. My suggestion is that it wasn't all these words, but it was the attention to testing, simple code, incremental development, and keeping the code clean.
See what you think!
Initial Thinking
Codea is a lovely Lua development system that runs on the iPad. I enjoy building things with it. Recently, on the forums at codea.io, "dave1707" posted an example of a little calculator, as an example to some new member. I thought I might put together a description of how I might do a similar thing.
I expect to find a lot of differences between Dave's calculator and mine, so I'll keep my functionality the same as his. Predicting what will come out, I expect that Dave's version will be many fewer lines of code. It will probably be more "efficient", though it's not clear just what efficiency means in a program that spends its time waiting for someone to press a button. Anyway, we'll see what happens, and in the end, everyone gets to do it the way they see fit.
I did read Dave's version and sort of figured out what it did. That might influence what I do now, but we're always building on everything we know. It may also have confused me, especially since I didn't type it in and run it. We'll see if we see his influence on this version, or not. I'm happy either way.
There will be a lot of words here, because I'll try to put down all my thinking. My experience is that it takes more than twice as long to write an article like this as it does to write the program. That's because we all think a lot faster than we type. At least I hope so ...
Calculator Operation
I started by drawing a little sketch of how this kind of calculator works.
My "theory of calculator operation" is that you have buttons, a display, and a temp register. The buttons come in digits, standard binary infix operators (+ - * / ^) and sin/cos, which are a different kind of operator, as we'll see. These are unary operators if you're into terminology.
When you press a digit key, that digit appends to the digits already in the display.
When you press an infix operator, the operation implied is performed between the display register and the temp register. The result goes to temp, and the display is cleared. When you press a unary key, the operation is immediately applied to the display and the result left in the display.
Now I'm pretty sure this is how calculators are supposed to work. It's possible, though, that I just don't get it. So I'd like to find out ASAP whether this design will work.
Program Design
Well, there will be buttons. I plan to rip off Dave's design for the buttons, although I expect I'll generate them a bit differently. We'll see what we need.
And I plan to build a calculator object, in a class. Dave put his functionality right in the buttons, which certainly works fine. My training and experience lead me to put separate function into separate modules, in this case, objects. If I were just whacking out a calculator, I might do it differently. I might even build something along Dave's lines and then refactor it to a design that I like better. (Of course I might just leave it alone, if it was working fine, especially if it was just something I did for fun.) But for today, I'm going to break out the calculator as a separate class. My plan is to test it as I go.
First Implementation
Begin with a test
As usual, I start with a test. I'm putting it in setup, and the initial cut looks like this:
c = Calculator()
c.press("1")
c.press("3")
c.press("*")
c.press("3")
c.press("=")
So I'm assuming I'll have a Calculator class, and that it understands a press
method. I've not checked any results yet but I expect to see display containing, in order, 1, 13, nothing, 3, 36, while temp will contain 0 (or nothing), 0, 13 ...
WOW, wait! My design is wrong! I'm thinking that when you type the '*' it will apply the operator multiply to temp and put the result into temp. But the temp at this point will be zero, so that won't do. It would zero the result.
I am tempted at this moment to read Dave's version and see what it does. But we won't always have Dave to rely on so I'm going back to the drawing board to figure out how these things work.
... thinking and sketching ensues ...
OK, I have a new theory of operation. The calculator has to remember the operator you type, and when you type a new operator, then it applies the old one. And then it remembers the new one.
I'm still thinking I can work with just one temp register but I'm uncertain.
There are a number of things one can do when uncertain. One of them is to think more. The other is to have the computer help you, by programming something to see what the program really wants to be. In the past, I used to design for a long time. My more modern practice is to do my design thinking, as much as possible, with the program there in front of me. So I'm going to extend my test to examine some results and then I'm going to code a Calculator.
Revised test
c = Calculator()
c.press("1")
c:displayIs("1")
c.press("3")
c:displayIs("12")
c.press("*")
c:displayIs("12")
c.press("3")
c:displayIs("3")
c.press("=")
c:displayIs("36")
Now, of course, I could just print the display after each step. And if I weren't trying to demonstrate a sort of "test-driven" approach, I might. After all, it's just a calculator. What I'm doing here, though, is different from testing by printing in a couple of ways.
First of all, I'm saying right in the code what I expect to happen. So I have designed and specified the behavior of the calculator, right here in plain sight.
Second, I'm setting up a test that I can repeat without looking at the printout and checking it by eye. In this case, it may not be worth it. Even here, it might be. In a larger program, that grows and changes for a longer time, this kind of test can be quite valuable in assuring me that I haven't broken anything. Because I surely will break something.
Now to write a calculator that can do this. I'm not sure what the displayIs function will do just yet but I'm thinking it'll print the display and say whether it's happy. It might do nothing unless it's not happy. We'll see what we like.
First Version -- just shows errors
Here's what I did:
Calculator = class()
function Calculator:init()
self.display = ""
end
function Calculator:press(key)
end
function Calculator:displayIs(string)
if self.display ~= string then
print("expected", string, "but got", self.display, "!")
end
end
I decided that I don't want the displayIs whining at me all the time, so it'll just print the exceptions. Of course right now, it gets everything wrong so all the displayIs calls print an error. I hope to fix that soon.
Next Version -- eliminate an error or two
So, OK. Next step is when a digit is pressed, append it to the display string. At least that's my plan. Here goes:
function Calculator:press(key)
self.display = self.display..key
end
Now you may find this interesting. All I'm doing is appending every keypressed to the display. This is clearly wrong! Why would I do such a stupid thing? Well, my story is that it's simple, not stupid, and I'm sticking to it. This "implementation" makes my first two tests run, and the rest still fail. So it is progress. I prefer to write as little code as possible between a test failing and success.
A concern people often raise about working this way is that I'll have to change a lot of code. We'll see. What usually happens is that I add code, but don't often have to replace or change much.
Mind you, I don't always take such tiny bites. It's easy to get on a roll and slam in a lot of code. Then, inevitably, I have to debug it. I don't like that: I'd rather have a program that works a bit, then a bit more, then a bit more. Your mileage may vary. I'm showing a particular way of working here. Take what you wish from it, ignore the rest.
Make it clear on star
Now I need to make another test run, the one where you type * and I expect the display to go from 12 to blank. Tell you what: I'll just code this much: "When the star is pressed, clear the display." That will give an interesting outcome I think.
function Calculator:press(key)
if key == "*" then
self.display = ""
else
self.display = self.display..key
end
end
The tests fail! Look at these lines:
c.press("3")
c:displayIs("12")
c.press("*")
c:displayIs("12")
c.press("3")
c:displayIs("3")
OK, well, that was just dumb. My calculator is supposed to do nothing on star, and then when you type another digit, it should clear.
Did you see that that was going to fail? Are you thinking "how dumb"? Well, I took a break for lunch between the last test and this one, and I forgot what the test was. I just coded what I thought it was. I should have looked but I started typing before I got my iPad out of the bag.
Oddly, this doesn't bother me, because my tests immediately tell me "how dumb". That's why I like having them: they remind me of what I was doing when I come back from lunch. What people who work this way will often do, before they leave for the day, is write one new test that fails. The failing test reminds them what to do when they come back.
If this all seems odd to you, I'm not surprised. It was odd to me when I started working this way, oh, 15 years ago. And I've shown a lot of other people how to do it, and it was odd to them at the start too. After a few days trying it, they get the idea and begin to use this approach -- in their own style -- when they think it's useful. Right now, you're probably thinking how dumb I am for not realizing that the display wasn't supposed to clear yet. I'm thinking how smart I was for having that test to catch me out. It's all in the point of view.
One other thing on this: no matter how smart you are -- and I will say right here that I am hella smart -- you will make programming mistakes, and most of them will be dumb. It's important not to take your dumb moments personally, but to learn from them. Just now I learned that I'm happy to have tests, and that I need to remember to look at them.
Back up - stop clearing on star
OK back to the drawing board. Obviously the star isn't supposed to do anything. So I can remove the code inside that part of the if. No, really, that's what I'm going to do:
function Calculator:press(key)
if key == "*" then
else
self.display = self.display..key
end
end
Guess what! My test runs. Now these last two are failing, like this:
c.press("*")
c:displayIs("12") -- OK
c.press("3")
c:displayIs("3") -- Displays 123
c.press("=")
c:displayIs("36") -- Displays 123=
So, what to do here? We have to make pressing 3 after star clear out the display and put three into it. (We know this will apply to all of 0 - 9, so I won't insult us all by just making it work for 3.) So somehow we have to know that a star happened, so that we'll clear the display before we append to it like always.
I wasn't sure what the calculator should do, so I ran Dave's. I noticed that if you type 3*6+2 into it, you get 8. The display, meanwhile, displays "3*6+2" Well, he said it was still buggy: his point was to show how to do certain things, not to build a real calculator. I kind of like it displaying the whole calculation but if it were to do that I'd like it to work, and that way lies trouble, because when we type 2+3*6 we mean that to be 2 + 18, not 5*6, because operator precedence. We don't want to go there. No, really, we don't. That's a whole different kind of calculator.
Make it clear on next digit after star
So I have to make mine work. I think I'll save the operator and if a digit comes in when an operator is saved, clear the display and start the append over again. (Yes, I know I'm supposed to do a calculation too but that's in a future test.)
I had to initialize my new variable, "op" in setup, so now I have this:
function Calculator:init()
self.display = ""
self.op = ""
end
function Calculator:press(key)
if key == "*" then
self.op = key
else
if self.op ~= "" then
self.display = ""
end
self.display = self.display..key
end
end
Now the only test that's failing is the =. It displays = and it should display 36. To make this work, when the equal comes up, I need to multiply what was on the screen (the 12) by what is on the screen (the 3). This means I'd better tuck the 12 away, at the same time as I save the star.
Do the math. Finally!
And I'd better handle the = separately too, using it to trigger the arithmetic. So I write this:
function Calculator:press(key)
if key == "*" then
self.op = key
self.temp = self.display
elseif key == "=" then
self.display = self.display * self.temp
else
if self.op ~= "" then
self.display = ""
end
self.display = self.display..key
end
end
This gives the surprising result in my test of expected 36 but got 36 !
. My guess is that when I do multiply of "12" and "3", I'm getting a blank in the result. To find out, I'll improve the test to show some delimiters:
function Calculator:displayIs(string)
if self.display ~= string then
local diag = "expected /"..string.."/ but got /"..self.display.."/!"
print(diag)
end
end
Much to my momentary surprise, it says "expected /36/ but got /36/!". So what's going on? Ah! What's going on is that the multiply operator returned the number 36 instead of the string "36", so the test failed. I'm not sure what to do. One simple thing would be to convert my numeric results back to strings. I wonder what would happen, though, if I just ignored the problem. Trouble with that is that I would have to fiddle the tests to deal with "36" versus 36.
I think I'll just convert back to string. What's a good way to do that? How about ""..(self.display*self.temp)
? That might work ... basically appending the number to an empty string in hopes that Codea will convert it for me. Failing that I'm going to have to look something up. Ha! That works. Kind of a hack but I'm happy for now.
By the way, did you notice that "12" * "3"
results in 36? Codea converts the strings to numbers before doing the arithmetic. How polite!
Anyway, my first test now runs. Let's review the whole thing:
First test runs!
-- Article Calc
-- Use this function to perform your initial setup
function setup()
c = Calculator()
c:press("1")
c:displayIs("1")
c:press("2")
c:displayIs("12")
c:press("*")
c:displayIs("12")
c:press("3")
c:displayIs("3")
c:press("=")
c:displayIs("36")
end
-- This function gets called once every frame
function draw()
background(40, 40, 50)
end
Calculator = class()
function Calculator:init()
self.display = ""
self.op = ""
end
function Calculator:press(key)
if key == "*" then
self.op = key
self.temp = self.display
elseif key == "=" then
self.display = "" .. (self.display * self.temp)
else
if self.op ~= "" then
self.display = ""
end
self.display = self.display..key
end
end
function Calculator:displayIs(string)
if self.display ~= string then
local diag = "expected /"..string.."/ but got /"..self.display.."/!"
print(diag)
end
end
Story complete. Pause and reflect.
Time to reflect a bit on what just happened. I wrote a short series of tests of the calculator and made them run one by one. I haven't added any code that wasn't needed to make those tests work -- at least I don't think so.
That is part of the test-driven development style (TDD) that I use and teach. We never write a line of code that isn't required by a failing test. So even though I know that there are other ops to worry about, plus minus and so on, I didn't put them into the if statement. I didn't cater to anything that wasn't implied by that test, to the best of my ability.
For this tiny app, it's not important. When building something larger, this discipline is a nice way to keep from gold-plating things, adding "generality" that isn't needed. I love to write general code. I have an advanced degree in this stuff and there's great joy in writing that sort of thing. These days, I take greater joy in letting the program seem to emerge from the fog.
YMMV. I'm not suggesting you should work this way. You might not even want to know this way. But if you want to be a professional programmer these days, it's valuable to be aware of these approaches. For the folks here, I just hope you find it interesting.
Now what? Well, we need another test. We could do a simple one with + or - or / and we'd learn a bit. But I am inclined to write a harder test, including one of those operators, and to learn from that.
Temptation - operations as anonymous functions
Looking forward, I can see there's going to be a lot of checking for star and plus and all those. One time to save them in self.op
and then probably later to apply them (where I just have a multiply now). I am really tempted to do something nicer than a bunch of if statements. And we are here to learn Codea/Lua, not just to be all up tight and supposedly professional. What if, when we see the star, we stored a function in self.op and when we need to execute that star (or plus or whatever it'll be) we just executed that function? That might be better.
Now when I'm operating at full discipline, I would build it with if statements and the look at it and find it ugly and improve it. This would be safe and easy, because my tests would support me. It's a bit against my best principles to put in this idea right now before it has shown to be fully required.
But hey, if you wanted a saint, you should have called someone else. I'm going to try it and see if I like it:
function Calculator:press(key)
if key == "*" then
self.op = function(a,b) return a*b end
self.temp = self.display
elseif key == "=" then
self.display = "" .. (self.op(self.display, self.temp))
else
if self.op ~= "" then
self.display = ""
end
self.display = self.display..key
end
end
Note the
self.op = function(a,b) return a*b end
and the corresponding
self.display = "" .. (self.op(self.display, self.temp))
In self.op, I stored an anonymous function returning a*b, and then just called that function when the equal came up. I plan to store similar functions for plus, minus and so on. Will I love this? We'll see.
Right now, it's time for a break. Next time, I'll write a new test that's a little longer and has another operator or two. We'll see what happens.
The Next Test
I decided to do a bigger test, basically 450 / 15 - 19, which if my calculations are correct comes to 11. I noticed in writing it that I had not made the calculator instance local in my test. So I'll provide the full text of the setup() function here.
function setup()
print("tests begin")
local c = Calculator()
c:press("1")
c:displayIs("1")
c:press("2")
c:displayIs("12")
c:press("*")
c:displayIs("12")
c:press("3")
c:displayIs("3")
c:press("=")
c:displayIs("36")
c = Calculator()
c:press("4")
c:displayIs("4")
c:press("5")
c:displayIs("45")
c:press("0")
c:displayIs("450")
c:press("/")
c:displayIs("450")
c:press("1")
c:displayIs("1")
c:press("5")
c:displayIs("15")
c:press("-")
c:displayIs("30")
c:press("1")
c:displayIs("1")
c:press("9")
c:displayIs("19")
c:press("=")
c:displayIs("11")
print("tests end")
end
It's getting a bit tedious to write these tests. Often, in this kind of situation, it is. I expect tests for a real calculator object to last for a long time but even so, if it's a pain I won't do it. One way to make it easier might be a helper method pressDisplaying(input, output) that would do the press and then check the result. That would save a lot of typing. With a little regex action I could do that here but Codea isn't as much help on that as some editors.
Anyway, this test calls for implementation of divide and subtract, plus it deals with chaining operations. We'll see how that goes. I haven't run the test yet. Stand back, this thing may explode.
First error is "expected /450/ but got /450//!". And I notice that I can't tell just which one of my expected 450s this is, but I'm pretty sure it's the one after the slash, since we know we don't cater to it and so it'll be treated as a digit. Makes me think the tests need a number or something. I'll keep it in mind.
Side Note
Are you amazed or shocked at how little pre-planning or up front design I do, or how I seem to tolerate things being a bit crufty for a while? That's on purpose. I am confident that my approach will bring all the issues into view, and that I'll work on the ones that are actually troublesome. And I'll skip the ones that turn out not to be a burr in my fundament.
This is rather different from the approach described in most of the older software development books, such as Code Complete. There's great material in those books but we in the Agile movement have observed that with today's tools and techniques, we can do more and more of our design as we go, not just up front.
I've been programming for over half a century (arrgh) and working in this style for 15 years, so I'm pretty comfortable with it. Not that it's just easy and automatic. I think programming will never be easy, because as my friend Ward Cunningham once said to me, everyone winds up working at the limits of their ability, because people keep piling harder problems on us as we get better. But we can work to keep things simple, if not easy.
Back to the code
I went ahead and put in slash in the same style as times. That caused me to see that order matters for divide and subtract (and power, if I do that). So I renamed the parameters for my anonymous functions to d and t, for display and temp, both in the new divide and back in multiply. It doesn't change the function, but makes the code more clear and consistent.
function Calculator:press(key)
if key == "*" then
self.op = function(d,t) return d*t end
self.temp = self.display
elseif key == "/" then
self.op = function(d,t) return t/d end
elseif key == "=" then
self.display = "" .. (self.op(self.display, self.temp))
else
if self.op ~= "" then
self.display = ""
end
self.display = self.display..key
end
end
The tests now run up to "expected /15/ but got /5/!". This surprises me. In the first test we got two numbers in. But! It was in the first argument, not the second. OK, look at where the digits go in. We're checking op against "", and if it isn't, we're clearing the screen. That will happen on every press, because the op is still hanging. In addition, we shouldn't use an empty string test anyway, since op is now a function, not a string.
The behavior we want is that the first digit after an op should clear and then go in, but not the following ones. Also, I noticed that in setting up for divide, I forgot to copy display to temp. So I put it in there. But now I'm wondering if we can combine the clearing and the copying until the first digit after an op. I'll try it. I plan to add a flag, named first
, and do the copy and clear based on that. Here goes.
function Calculator:press(key)
if key == "*" then
self.op = function(d,t) return d*t end
self.first = true
elseif key == "/" then
self.op = function(d,t) return t/d end
self.first = true
elseif key == "=" then
self.display = "" .. (self.op(self.display, self.temp))
else
if self.first then
self.temp = self.displat -- typo discovered below!
self.display = ""
self.first = false
end
self.display = self.display..key
end
end
With this in place, the tests run with no comment until an error, on line 11, attempt to perform arithmetic on local t. This leads me to the typo above, referring to self.displat. Oops.
function Calculator:press(key)
if key == "*" then
self.op = function(d,t) return d*t end
self.first = true
elseif key == "/" then
self.op = function(d,t) return t/d end
self.first = true
elseif key == "=" then
self.display = "" .. (self.op(self.display, self.temp))
else
if self.first then
self.temp = self.display
self.display = ""
self.first = false
end
self.display = self.display..key
end
end
Fixing that gives me "expected /30/ but got /15-/!", which is, of course, the minus failing to be known as an operator.
So we code minus:
elseif key == "-" then
self.op = function(d,t) return t-d end
self.first = true
Now we get "expected /30/ but got /15/". Hmm. I think we clobbered the divide op before doing it! Hey, look, we only actually ever perform an operation on equals! We need to do any pending op before ever putting the next one in.
I also notice a lot of duplicated lines here. Duplication is the code's way of telling us that there is an idea in our head that is not reflected directly in the code. We'll want to clean that up soon. But I don't like to do much code improvement while the code isn't working. It's too easy to introduce new errors and not notice them. So I'll press forward, noting the duplication but living with it until these tests run, if I can. If it gets too hairy, that will be a sign that I've bitten off too much. Let's hope not.
The tests run
And the tests run to completion! I added a doPending()
function and used it as needed. Nothing else that I recall:
Calculator = class()
function Calculator:init()
self.display = ""
self.op = nil
self.first = false
end
function Calculator:press(key)
if key == "*" then
self:doPending()
self.op = function(d,t) return d*t end
self.first = true
elseif key == "/" then
self:doPending()
self.op = function(d,t) return t/d end
self.first = true
elseif key == "-" then
elseif key == "=" then
self:doPending()
else
if self.first then
self.temp = self.display
self.display = ""
self.first = false
end
self.display = self.display..key
end
end
function Calculator:doPending()
if self.op == nil then return end
self.display = "" .. (self.op(self.display, self.temp))
self.op = nil
end
function Calculator:displayIs(string)
if self.display ~= string then
local diag = "expected /"..string.."/ but got /"..self.display.."/!"
print(diag)
end
end
function Calculator:draw()
end
function Calculator:touched(touch)
end
Note the new doPending()
function. If there's an op present, it executes it and clears the op. It's called right before installing an op. Let me mention explicitly the if at the beginning of that function. I could have said if self.op ~= nil then
and then done the statements. Same effect. The way I've done it there is called "Guard Clause" (see Kent Beck's books on coding patterns). The idea is that you check any special cases at the top and then exit. If the code got at all complex there, I'd do it with the more usual if/elseif/else
, of course. Guard Clause is a pattern I use commonly. If the team's coding standard uses it, then the team will be used to it. If not, then really no one on the team should be using it. I'm my own team right here, so we do it my way.
Refactoring - duplication means the design needs improvement
As I predicted, we see duplication now. Each of the operators follows the same pattern. Each does any pending op, then puts its own special function into self.op
, and then sets the first flag.
As I mentioned, duplication tells us that the code has an idea in it (or our head does) that is not as explicit in the code as it could be. Duplication is the enemy of clear code and the enemy of easy maintenance. If we have this copy-pasted patch of code all over, then when we have to change it -- and surely we will -- we have to change it everywhere. And we'll likely miss one. Or worse yet, we might have code that is nearly duplicated, looks the same, but with subtle differences. If we extract the duplicated part, we can see the differences more readily. That gets us to working, clean, maintainable code sooner.
Now that my tests are working, I can clean up the code. In test-driven development (TDD), we call this "Red / Green / Refactor". When the tests are Red (not running) we do not refactor if we can possibly avoid it. We make the tests go Green. When they run, then we look at the code and see how to improve it. It's time to do that now. We could wait until we put in plus, and power, or we could do it now. If we wait, there will be more duplication. Right now we have three cases, and that's usually a good basis for making the improvement. What shall we do? The three offending lines look like this:
self:doPending()
self.op = function(d,t) return t-d end
self.first = true
Each occurrence has a different function in the middle and the other two lines alike. I can think of a number of ways to improve it. Let's look at doPending()
as well, since it's used in the dups:
function Calculator:doPending()
if self.op == nil then return end
self.display = "" .. (self.op(self.display, self.temp))
self.op = nil
end
What if we change doPending()
to accept the function as a parameter. Then it can update the display, set the first flag, and store the new op all at once. Sounds tempting.
However, the equals operator calls doPendng()
but doesn't do the other things. Perhaps it should. It could certainly take nil as the function to set up next, and it seems likely that setting first will be a good idea, since if after displaying the result, the next digit typed does in fact want to clear the display.
The bad news is, there goes my Guard Clause. That didn't last long this time, but it's still a good bet because -- by my standards of code reading -- it's more clear when it does apply. YMMV of course, as with all of this.
Anyway, doPending()
. We'll pass in a function to be saved away in self.op
. We'll always set the first flag. If there's an existing op waiting, we'll execute it, otherwise not. This can be coded in a couple of ways. I could probably save my Guard Clause, but it would have to occur in the middle. That's not kosher for Guard Clause. So I extend doPending()
:
function Calculator:doPending(func)
if self.op ~= nil then
self.display = "" .. (self.op(self.display, self.temp))
end
self.first = true
self.op = func
end
And then use it:
function Calculator:press(key)
if key == "*" then
self:doPending(function(d,t) return d*t end)
elseif key == "/" then
self:doPending(function(d,t) return t/d end)
elseif key == "-" then
self:doPending(function(d,t) return t-d end)
elseif key == "=" then
self:doPending(nil)
else
if self.first then
self.temp = self.display
self.display = ""
self.first = false
end
self.display = self.display..key
end
end
It's alive!
And the tests still run! This gives me great confidence that I haven't broken anything. The code is pretty clean so far, I'd say. We need to implement the + and ^ operators, and sin and cos if we're going to copy Dave, but it's bedtime here at the ranch. I'll decide tomorrow whether to publish this as one, article, or three. It's over 5000 words now, so I think three, or even four, might be a good idea. I'll decide when next I get back to this. For now, everything is solid and rather clean, and I can rest easy.
A Note
Before I go, a note to myself. The else clause in the press()
function accepts digits but also any unimplemented operators. We have seen that happen, with the slashes showing up at the end of the display. (Dave's calc actually does that. I think he intended it, as it shows what's going to happen in the future. I do not intend it.) Anyway, we should probably limit the else to the digits and put in an else clause that gives us some kind of error if we get an unexpected character. But that's for tomorrow.
In fact, it was for Friday
Hello, Friday morning here. I've been doing other things that I was supposed to be doing. Now let's see if we can finish up this article, at least to a point where it makes sense to publish it. (Sense by my standards. YMMV.)
I've pretty much decided that I'm going to stop with the four operations plus minus times divide, without sin/cos, but I might do them just to show that I can. Or maybe +/-, the unary sign-changing operation, which is commonly present on "four-function" calculators.
What to do
When I return to a project after a day or a month, I look around to see what needs to be done. Sometimes there is an intentionally broken test waiting, to remind me where to go. I did not do that way back Wednesday, because I knew I was leaving for a day or two and I don't like a system left on a red bar for very long. And anyway I knew that minus was the only operator left untested and unimplemented. No, wait, it's plus. Wait ... two days and I don't know quite where I left off.
The only way to know is to look at my notes ... or the code ... or the tests. I'll pick the tests. Time to review them anyway. They look like this:
-- Article Calc
-- Use this function to perform your initial setup
function setup()
print("tests begin")
local c = Calculator()
c:press("1")
c:displayIs("1")
c:press("2")
c:displayIs("12")
c:press("*")
c:displayIs("12")
c:press("3")
c:displayIs("3")
c:press("=")
c:displayIs("36")
c = Calculator()
c:press("4")
c:displayIs("4")
c:press("5")
c:displayIs("45")
c:press("0")
c:displayIs("450")
c:press("/")
c:displayIs("450")
c:press("1")
c:displayIs("1")
c:press("5")
c:displayIs("15")
c:press("-")
c:displayIs("30")
c:press("1")
c:displayIs("1")
c:press("9")
c:displayIs("19")
c:press("=")
c:displayIs("11")
print("tests end")
end
With a bit of inspection, I see that there is no test of plus. I also see that the tests are harder to read than I might like. And they were a pain to write. I think I'd prefer something like this:
c:check("4","4")
c:check("5","45")
where check
presses the first parameter and checks the display for containing the second. This would readily be implemented by calling the other two functions, of course.
We might also wish to build that function into our testing, rather than into the Calculator. It's considered to be a bit off to have functions in production code that are only intended for testing. And it would be easy enough to do it in setup.
There are other "nice" things we might do with the tests. We could have a checkBegin
and checkEnd
function, for example, that might print something like "....X.X." if tests 1-4, 6, and 8 passed, but 5 and 7 did not. The fact that the tests print nothing when everything is OK is a bit scary.
Why do you bring this up?
Good question. I bring this up because, today, the top "Agile" teams actually think about this stuff and do something about it. With modern refactoring tools it's easy to make changes like this, and if we were going to be looking at this code for months, making it easier to understand is worth the effort
So I mention it because part of my purpose in life is to let people know how things are being done. I'm not saying you need to do them. I'm just explaining what I do, what other "Agilists" do, and why we do them. Then you get to see the code and the results and decide for yourself what, if anything, is worth trying. I hope you'll try a lot of these ideas, but I don't get paid any more -- or any less -- if you don't.
Anyway, plus ...
I'm feeling pressure to get this article out. And that pressure, self-imposed though it is, is telling me not to clean up the tests but to Build That Feature. And that's what I'm going to do. But I'm aware that even a tiny bit of self-imposed pressure is enough to cause me to slack a bit on the things that I know darn well make my code more able to be changed in the future.
You have to make trade-offs. I'm making this one. But even now, there's a bit of me whispering that I'm doing the wrong thing. Hell, I could have fixed the tests up by now.
Oh, OK, stop nagging. First I'll fix the tests. Hold on.
That was grueling ...
I decided to do the change at 9:40. It's 9:44. I took the code into Sublime, did a find/replace with a regular expression, and now it looks like this:
-- Article Calc
-- Use this function to perform your initial setup
function setup()
print("tests begin")
local c = Calculator()
c:check("1","1")
c:check("2","12")
c:check("*","12")
c:check("3","3")
c:check("=","36")
c = Calculator()
c:check("4","4")
c:check("5","45")
c:check("0","450")
c:check("/","450")
c:check("1","1")
c:check("5","15")
c:check("-","30")
c:check("1","1")
c:check("9","19")
c:check("=","11")
print("tests end")
end
I'll paste that back to Codea. It will fail for lack of check, which I'll implement:
function Calculator:check(key, result)
self:press(key)
self:displayIs(result)
end
And Bob's your uncle, whatever that means. Anyway it was totally easy and took just a few minutes to do. And I feel better now. Lesson learned? Probably not, I am perpetually lazy. The moment you guys look away I'm gonna do something lazy. Trust me.
CloudClip
By the way, got a neat free tool called CloudClip Manager, for the iPad and the Mac. It shares the clipboard across iCloud. So I can copy something on my Mac (where I'm writing this) and paste it into Codea. Or vice versa.
Because the iPad doesn't multi-task at all well, you have to flip into CloudClip to get it to refresh through iCloud but it is still ten times easier than mailing the code to yourself or whatever I used to be doing. Recommended product. I have nothing to do with it and do not get a percentage of the (free) price. Or 100 percent of it. I forget.
Plus. We were going to do plus.
I think I'll just add a plus operation to an existing test. That should be good enough to get plus working. Then maybe I'll add another test. So, looking at the last test above, we end up with 11. I could add a +5 before the = and get 16. Or, might be interesting to add the +5 after the = just to see what it does. I've not specified that.
I'm going to try it but when it fails, I'm going back to before the = and get that working first, because otherwise I'd be building two features at once, plus and input after equal. That would be bad. Here goes ...
Well, the first thing that happens is "Expected 11 but got +". We know that's because plus doesn't work at all yet. So I'll put in the obvious lines for plus.
And the tests run! Woot! Here's all the code:
--# Calculator
Calculator = class()
function Calculator:init()
self.display = ""
self.op = nil
self.first = false
end
function Calculator:press(key)
if key == "*" then
self:doPending(function(d,t) return d*t end)
elseif key == "/" then
self:doPending(function(d,t) return t/d end)
elseif key == "-" then
self:doPending(function(d,t) return t-d end)
elseif key == "+" then
self:doPending(function(d,t) return t+d end)
elseif key == "=" then
self:doPending(nil)
else
if self.first then
self.temp = self.display
self.display = ""
self.first = false
end
self.display = self.display..key
end
end
function Calculator:check(key, result)
self:press(key)
self:displayIs(result)
end
function Calculator:doPending(func)
if self.op ~= nil then
self.display = "" .. (self.op(self.display, self.temp))
end
self.first = true
self.op = func
end
function Calculator:displayIs(string)
if self.display ~= string then
local diag = "expected /"..string.."/ but got /"..self.display.."/!"
print(diag)
end
end
function Calculator:draw()
end
function Calculator:touched(touch)
end
--# Main
-- Article Calc
-- Use this function to perform your initial setup
function setup()
print("tests begin")
local c = Calculator()
c:check("1","1")
c:check("2","12")
c:check("*","12")
c:check("3","3")
c:check("=","36")
c = Calculator()
c:check("4","4")
c:check("5","45")
c:check("0","450")
c:check("/","450")
c:check("1","1")
c:check("5","15")
c:check("-","30")
c:check("1","1")
c:check("9","19")
c:check("=","11")
c:check("+","11")
c:check("5","5")
c:check("=","16")
print("tests end")
end
Notice the check for + 5, there at the end.
Summing Up
Are we done? Well, we're probably all more than done reading this article, but if we were building a production calculator, we can think of more things to do. There certainly could be more functions, change sign, sine, cosine, arc-cotangent, and so on. Two- and three-dimensional plotting. I don't know: the people who want products think of a lot of stuff. That's up to whoever is deciding what the product needs to do.
Right now, that's me, and I've decided the product has done its job, which is to serve as a platform for talking about how I might implement something in Codea.
But there are things we might yet do if this were a real business project. First, we might want more tests. These were the bare minimum. We might want to deal with errors like dividing by zero, or errors like typing one operator after another. And so on. We should test all those and make them do whatever we decide is right.
That can get bloody tedious. That's why programmers in real life get paid, I suppose. But frankly, I'd rather do this than ship a product that has bugs in it, and I'd rather code this way than leave code lying around for the unsuspecting future me to look at and go WTF????
Practices -- and do you really do this??
Let's look at some of the key aspects of what I've done in this exercise.
Quick Design
First, I started with a little bit of design. I drew a picture of how I thought a calculator works. Remember that I got it wrong. The more we design without feedback, the more we get bits wrong. We're tempted to say "we should have designed more", but I believe the truth is "we should have gotten concrete feedback sooner".
Begin with tests
Then, I did move quickly to an implementation. I started with tests. Automated tests cause me to think a bit more carefully about what I'm doing, and they can be run again and again to be sure I don't break anything. And once in a while, when a test "fails", I look at it and realize that my expectation as expressed in the test is wrong: my understanding of what I'm doing is wrong.
When a test fails, I always learn something. Usually I learn that I've not yet built a feature. Sometimes I learn that I've broken an old feature. Sometimes I learn that I don't know what I want. Sometimes I learn that I fumble-fingered in the test or in the code. It's all good. When the tests work, I have growing confidence in the program, and when they fail, it alerts me to an incoming learning.
Build from a simple (nearly stupid) design
Remember that I started by just appending every key press to the displayed answer. Some would call that stupid: we in the Agile programming universe call it simple. (Sometimes we return a constant for a variable answer. If it's the correct constant for our current test, we call that pattern "Fake it til you make it". I don't think I did that here.)
Appending the key press isn't wrong ... it's just wrong sometimes. So I do it, and some tests pass. When one fails, I've discovered when "sometimes" is.
Just make the test pass
Now, trust me, I am fairly good at programming, so I really did know that times was going to show up in the display, and that that was bad. But I didn't code for that. I coded to make the next test pass.
What I do is make the test pass with the simplest code I can write. Not bad code, but simple. I make the code "more nearly" do the right thing. Inch by inch, step by step (slowly he turned), I make the program work, guided by my tests. I try never to write a line of code that isn't required by the test I'm working on.
Refactor to make the code clean
I'm always watching for code that is not clean. It might be in my tests, and it will certainly be in the production code.
When my tests are green -- all running -- I look for ways to improve it. I changed the code to use an anonymous function at one of those points, and it worked out well.
Avoid the esoteric
Anonymous functions are pretty popular these days, in some circles, but to some programmers they are quite odd because they may have never used a language where they are possible.
I don't use them often myself so I wanted to try it. Against doing that was my own lack of practice, and the fact that I'm kind of writing to beginners with Codea. In favor of doing it was that it's an important thing to be good at in Lua, and that I had my tests to guide me.
Is the anonymous function too esoteric for you? Then don't do it. And if you do decide to try something cool, make sure it's surrounded by tests. Some teams have a rule that if anyone every writes something "cool", it has to be deleted. Cool code is the code that bites you at 2 AM when you are called in to get the system back up.
So, use judgment.
Bottom line -- nearly
All these words aside, if you'll think about what we did here, it went incredibly smoothly. We wrote some tests, made them work one after another, worked very simply, cleaned up the code as we went. We had no long debugging or "what the heck is going on" sessions.
Click click click. That's how it goes when I work this way. Might it work for you? I think it might.
Enough already
OK, this is more than enough for a single article. But I've decided not to split it apart. I'll warn readers at the top to consider small bites. If you made it all the way down here, congratulations and thanks for reading.
You can offer feedback on the codea.io forums if you're a member, or drop me an email. If there are interesting questions, I'll update this or write a follow-up. For now, thanks for tuning in.