Codea Calculator II
Ignatz and Jmv38 on the Codea forums commented on the previous article. I had hoped to do more anyway so here's the next one.
Note that this article is different from what it might have been, because I've had conversations with other programmers looking at (working with) the code. This isn't bad, it's good. It's like pair programming without all the travel. Be aware, though, that many of the "discoveries" in this article have already been discovered. There's a long note on the forum replying to their comments. This article will follow that note's line of reasoning, but I plan to rewrite it rather than copy and edit.
Because that's what I do.
GUI
Ignatz and Jmv38 talked about why I hadn't built the GUI and whether I should. I plan to do so, but this isn't that article.
It's worth mentioning here that I find GUIs hard to test, so I like to write really solid "model" objects, test the heck out of them, and then write thin GUIs where "nothing can go wrong". Or at least very little.
Even so, some testing is probably needed, and I think we'll discover that in the GUI article when I get to it.
Duplication
There is significant duplication in the calculator as it stands. Take a look:
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
There are four or five if statements, checking a key and then doing a do-Pending. And inside each doPending there is a function definition that looks exactly the same except for the operation done inside. (I note that they all are in t then d order except the first one. That's because I did a non-commutative operator second and t has to be first. I should change the first one to match, and I will. (I just did, and the tests all run.))
We'd like to remove this duplication. Perhaps I should stop and say why.
Why we remove duplication
I've mentioned some of the concerns already. If there is duplication in the code, that tells us that there is some idea, either in our mind or emerging in the code, that is not well-represented in the code. Here, the idea is "hey, all these operators are alike".
If there is duplication and changes are needed, we have to make the changes in many places. This is tedious and error-prone.
Here's a thing I haven't mentioned in this series yet. Long ago, Kent Beck (creator of Extreme Programming) listed the elements of "simple code". His list has taken a few forms but the one I prefer is this one:
Simple code:
- Runs all the tests.
- Contains no duplication.
- Expresses all the programmer's ideas.
- Minimizes the number of programming entities: lines, classes, files, etc.
These items are in priority order.
Now if you think about these, you might think "But wait, expressing ideas might require duplication. Numbers 2 and 3 should be reversed." Well, you might be right. If there were ever a conflict. But I prefer them in this order, because more often than not, duplication to "express an idea" really means that the code contains an idea that I have not as yet recognized. So I like to feel the conflict.
The list does come in the other order and in other forms. Find one you like and use it. I like this one.
Anyway, we've got this code: what shall we do about it?
Issues
I see two issues with those ifs. First of all, they are clearly very redundant. The duplication should be removed. But they are also a bit unclear: those embedded functions inside the doPending inside the then make the code tricky to read, especially since we don't expect to see functions written in line like that. At least I don't.
Both the following ideas come from Jmv38. We could break the functions out like this:
function Calculator.mult(d,t) return t*d end
function Calculator.divide(d,t) return t/d end
function Calculator.minus(d,t) return t-d end
function Calculator.add(d,t) return d+t end
Then we'd say, for example:
if key == "*" then
self:doPending(self.mult)
and so on. That might be a bit more clear but still leaves us with duplication.
Maybe we would look at this and realize that there is a one-to-one relationship between the character pressed and the function to be executed. (And the function has that character in the middle of it, but I can't figure a way in Codea to just do that. Unless I compile on the fly. No. Just no.)
So Jmv38 suggests that we build a local table, indexed by the key pressed:
local operations = {}
operations["*"] = function(d,t) return t*d end
operations["+"] = function(d,t) return t+d end
operations["/"] = function(d,t) return t/d end
operations["-"] = function(d,t) return t-d end
Now as it happens, I didn't realize that one could define a local table that way. It's "obvious" when you think about how Codea works but it wasn't obvious to me. This is why pair programming is so powerful. The other person knows stuff you don't know and thinks of stuff you don't think of.
We're left with the question of how to access the table. I think I prefer this:
function Calculator:press(key)
if key=="*" or key=="+" or key=="/" or key=="-" then
self:doPending(operations[key])
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
Are you starting to wish these things were in order plus minus times divide? I am. Something in my brain wants them to be that way. I'll fix that because what good is having a touch of OCD if you don't give in to it once in a while?
Done, and the tests still run. I feel better now.
Now Jmv38 suggests going a step further with
local f = operations[key]
if f then
doPending(f)
elseif key == "="
...
Nothing wrong with that, probably. But it does require us to look back and say "When could f be nil? Oh, if key isn't one of those operators. Hmm ..."
I don't like things that make me go "Hmmm". So I prefer the or. However, there are a lot of binary operators in the world, and that if could get pretty long. Jmv38's approach extends to new operators smoothly. So it could be better. I prefer it more explicit. YMMV.
Summing up ...
First of all many props to Ignatz and Jmv38 for their ideas and questions. We wouldn't be here without them.
I'll show the relevant code one more time below. First, the discussion. We saw more duplication, and we thought of ways to remove it. We also saw some code that didn't express itself well. We considered ways of fixing it but (with help) we saw an approach that seemed to promise help on both dimensions. We tried it, we liked it.
But those tests!! Without those tests, I'd have been scared as hell about making a major change like a table of functions and removing all my if statements. It would be too easy to screw it up.
The tests give us confidence that when we screw up, we'll find out before anyone else does. That's a very good thing.
Next time, the GUI. Might be soon, might not. I've got a gig next week.
Current Code
--# Calculator
Calculator = class()
function Calculator:init()
self.display = ""
self.op = nil
self.first = false
end
local operations = {}
operations["+"] = function(d,t) return t+d end
operations["-"] = function(d,t) return t-d end
operations["*"] = function(d,t) return t*d end
operations["/"] = function(d,t) return t/d end
function Calculator:press(key)
if key=="+" or key=="-" or key=="*" or key=="/" then
self:doPending(operations[key])
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
-- This function gets called once every frame
function draw()
background(40, 40, 50)
end