Codea Stats 7: Output
Let’s get some output up in this baby. I plan to steal two good ideas while I do this.
The point of this exercise is to provide interesting and possibly useful information about a Codea project. As such, its about time to get started working out what that information might be, and how to display it.
I have in mind to steal two ideas that I found in the code of Dave1707 from the Codea forum.
- Scrolling the Display
- Dave was displaying a long text list on the screen, and arranged to scroll it up and down by dragging one’s finger up and down on the screen. He did this neatly by calculating the delta-y of the finger motion, and applying that same value to the text display, so that it exactly follows your finger. I plan to borrow that idea.
- Paste from Codea Print
- I knew that when Codea prints something in the little console to the left, and you touch a line there, it copies it to the pasteboard. I use that capability to copy error messages into these articles. Touch the line, it copies, put cursor where I want the message, Command-V paste.
-
What I didn’t know is that you can check, inside the Codea program, whether there is anything on the pasteboard. With a little judicious clearing, you can detect that the operator has touched a line, and you can read the contents of the line they touched. Dave used this to print a list of projects to the console, and when you touch one, Voila! it displays that one’s output. This, too, will be borrowed.
Those things are not likely to happen today, but I wanted to get them documented so that what we do can take them into account, if it needs to, and to document my “larger” plan.
How Should We Proceed?
There are a few things that will need to be done before this all works nicely, and we can probably do them in a number of different orders. I guess the first thing will be to produce enough output to make scrolling interesting.
What reporting do we have so far? I don’t remember. Do you? Let’s look.
We have this test:
_:test("ProjectDescription classes", function()
local desc = ProjectDescription("SampleStatsProject")
local classes = desc:classes()
_:expect(classes:count()).is(2)
end)
_:test("ProjectDescription classReport", function()
local desiredReport = [[Classes
SampleCode
SubclassOfSample(SampleCode)
]]
local report = ProjectDescription("SampleStatsProject"):classReport()
_:expect(report).is(desiredReport)
end)
That’s a pretty simple report, but it could get long on a large program. Is there even a method report at all?
function ProjectDescription:classReport()
report = "Classes\n\n"
report = report..self.classCollection:report()
return report
end
function ClassCollection:report()
local tab = " "
local result = ""
for _i,classDef in ipairs(self.classDefinitions) do
result = result..tab..classDef:report().."\n"
end
return result
end
function ClassDefinition:report()
local result = self.className
if self.superclassName and self.superclassName ~= "" then
result = result.."("..self.superclassName..")"
end
return result
end
There is some corresponding code for Methods:
function MethodCollection:report()
local tab = " "
local report = ""
for _i, methodDef in ipairs(self.methodDefinitions) do
report = report..tab..methodDef:report().."\n"
end
return report
end
function MethodDefinition:report()
return self.className..":"..self.methodName.."("..(self.args or "")..")"
end
ProjectDescription doesn’t have a methodReport
method. Do we have tests for those two methods above? We do have a test:
_:test("MatchMaker finds methods", function()
local expected = [[ SampleCode:init(x)
SampleCode:draw()
SampleCode:touched(touch)
SubclassOfSample:init(ignored)
SampleCode:extraMethod(a,b,c)
]]
local mm = MatchMaker:methodDefinition()
local tab = readProjectTab("SampleStatsProject:SampleCode")
tab = stripComments(tab)
mm:process(tab)
local coll = mm:collection()
local isClass = coll:is_a(MethodCollection)
_:expect(isClass, "method collection").is(true)
_:expect(coll:count()).is(5)
local rep = coll:report()
_:expect(rep).is(expected)
end)
That’s a bit invasive, pulling the collection out of the MatchMaker, but it does do some key checking. So that’s good.
Let’s contemplate a “full report” and see about creating it. I think we’ll have to build it up bit by bit, and I expect to find a few things that get in the way of getting what we want.
Remind me to talk about another way we could have done this program.
Anyway … I’m thinking that a “full” report will be a list of class definition reports, in order by class name, each one followed by method reports for that class’s methods. I’m not sure about ordering the methods. For now, we’ll allow the order of occurrence. That way, if there’s any logic to my arrangement of methods in the tab, that’ll be preserved.
I don’t want to TDD this, but I suspect that I should. We’ll try to make it painless. I need a new test for full report.
_:test("ProjectDescription fullReport", function()
local desiredReport = [[Classes
SampleCode
SubclassOfSample(SampleCode)
]]
local report = ProjectDescription("SampleStatsProject"):fullReport()
_:expect(report).is(desiredReport)
end)
These multi-line text constants really confuse the Codea editor. Must find a better way. Anyway, this will fail looking for fullReport
, and I just left random text in the desiredReport
, from the class report. We’ll start with that.
5: ProjectDescription fullReport -- TabTests:50: attempt to call a nil value (method 'fullReport')
Let’s be about implementing that.
function ProjectDescription:fullReport()
report = "Classes\n\n"
report = report..self.classCollection:fullReport()
return report
end
This just defers to classCollection. I have in mind that for this to work, we’ll have to pass our method collection to full report, but that should be OK. Maybe. We’ll see.
This will fail looking for the method on classCollection.
5: ProjectDescription fullReport -- TabTests:88: attempt to call a nil value (method 'fullReport')
Same problem, later on. Implement: We have this method as a starting idea:
function ClassCollection:report()
local tab = " "
local result = ""
for _i,classDef in ipairs(self.classDefinitions) do
result = result..tab..classDef:report().."\n"
end
return result
end
We could make the test run right now, by calling that. Let’s do.
function ClassCollection:fullReport()
return self:report()
end
I expect the test to run. It does. Make it harder.
Let’s have the report include how many methods each class has.
local desiredReport = [[Classes
SampleCode (4)
SubclassOfSample(SampleCode) (1)
]]
We want the number of methods, in parens, after the class name. That fails of course:
5: ProjectDescription fullReport -- Actual: Classes
SampleCode
SubclassOfSample(SampleCode)
, Expected: Classes
SampleCode (4)
SubclassOfSample(SampleCode) (1)
Now we have to make it work. To do that, we certainly need the methods collection from the project description.
function ProjectDescription:fullReport()
report = "Classes\n\n"
report = report..self.classCollection:fullReport(self.methodCollection)
return report
end
function ClassCollection:fullReport(methodCollection)
local tab = " "
local result = ""
for _i,classDefin in ipairs(self.classDefinitions) do
result = result..tab..classDef:fullReport(methodDefinitions)
end
end
I decide to defer down to the classDef
. I expect trouble with newlines. Won’t be the first time or the last. Test should fail looking for fullReport
.
5: ProjectDescription fullReport -- TabTests:203: attempt to call a nil value (method 'fullReport')
Implement:
function ClassDefinition:fullReport(methodDefinitions)
local result = self:report()
local count = methodDefinitions:count()
result = result.." ("..count..")\n"
end
I think this is going to work. However:
5: ProjectDescription fullReport -- TabTests:179: attempt to index a nil value (local 'methodDefinitions')
We have no method definitions. That’s odd.
function ClassCollection:fullReport(methodCollection)
local tab = " "
local result = ""
for _i,classDef in ipairs(self.classDefinitions) do
result = result..tab..classDef:fullReport(methodDefinitions)
end
end
Could I possibly use the same name five lines apart? No, apparently not.
function ClassCollection:fullReport(methodCollection)
local tab = " "
local result = ""
for _i,classDef in ipairs(self.classDefinitions) do
result = result..tab..classDef:fullReport(methodCollection)
end
end
Let’s change the other to use that name.
function ClassDefinition:fullReport(methodCollection)
local result = self:report()
local count = methodCollection:count()
result = result.." ("..count..")\n"
end
Test.
5: ProjectDescription fullReport -- TabTests:209: attempt to concatenate a nil value
That says count is nil. Even if it were not, this code can’t be right, it’s not conditioned in any way by the class we’re looking at. The input is the method collection for the whole program. We should be using the methodsForClass
somewhere. But why did we get a nil?
Apparently I’m confused about what’s going on here. I’m afraid that I need more information, and that I’m going to resort to a print to get it.
function ClassDefinition:fullReport(methodCollection)
local result = self:report()
local count = methodCollection:count()
print("count= ",count)
print(methodCollection:report())
result = result.." ("..count..")\n"
end
count= 5
SampleCode:init(x)
SampleCode:draw()
SampleCode:touched(touch)
SubclassOfSample:init(ignored)
SampleCode:extraMethod(a,b,c)
That sure looks like it should have worked. I look more carefully at the error and then at the code that is erroring:
function ClassCollection:fullReport(methodCollection)
local tab = " "
local result = ""
for _i,classDef in ipairs(self.classDefinitions) do
result = result..tab..classDef:fullReport(methodCollection)
end
end
I reckon I’ve made my standard error of not returning the answer.
function ClassDefinition:fullReport(methodCollection)
local result = self:report()
local count = methodCollection:count()
result = result.." ("..count..")\n"
return result
end
That should work somewhat better. Well, sort of, now we have this:
5: ProjectDescription fullReport -- TabTests:88: attempt to concatenate a nil value
Let’s carefully look at line 88.
function ProjectDescription:fullReport()
report = "Classes\n\n"
report = report..self.classCollection:fullReport(self.methodCollection)
return report
end
The ..
line is the culprit. Does fullReport
return a result?
function ClassCollection:fullReport(methodCollection)
local tab = " "
local result = ""
for _i,classDef in ipairs(self.classDefinitions) do
result = result..tab..classDef:fullReport(methodCollection)
end
return result
end
It does … now. It didn’t … before. Let’s see if we can get this test passing, then let’s see how I can better avoid this error, which I make pretty much every time I use an accumulating variable.
Now I get an error I can respect:
5: ProjectDescription fullReport -- Actual: Classes
SampleCode (5)
SubclassOfSample(SampleCode) (5)
, Expected: Classes
SampleCode (4)
SubclassOfSample(SampleCode) (1)
We’re not conditioning the count by class name. Therefore:
function ClassDefinition:fullReport(methodCollection)
local result = self:report()
local count = methodCollection:countForClassName(self.className)
result = result.." ("..count..")\n"
return result
end
And …
function MethodCollection:countForClass(className)
return self.methodsForClass(className):count()
end
I noticed that the other method was “forClass” not “forClassName” so I’ll rename it in the call for consistency.
function ClassDefinition:fullReport(methodCollection)
local result = self:report()
local count = methodCollection:countForClass(self.className)
result = result.." ("..count..")\n"
return result
end
This should actually work, unless it doesn’t.
5: ProjectDescription fullReport -- attempt to index a nil value
That’s not helpful. Turns out I have a dot where I needed a colon:
function MethodCollection:countForClass(className)
return self:methodsForClass(className):count()
end
The test runs. Commit: First version of ProjectDescription:fullReport.
OK, it’s Saturday. Let’s retro and sum up.
Retro and Summary
I see two main topics to address in our retrospecting.
I wanted to talk about another way we could have done this program, and there’s this repeated error of not returning an accumulating local value.
Another way …
Our objects here, and there are a lot of them, were created in large part without our real user class, ProjectDescription, involved. I posited things like Class Definitions and Class Collections, and just TDD’d them into existence. The TDDing was good, but the choices of objects were speculative.
Of course they are also obvious choices. We know we’re going to need a definition for each kind of thing we report on, and we know we get them by matching patterns, and we know we want to get them all in one pass over the program. So those objects make sense.
But we were not much driven by how to use those objects, only by the obvious (and quite credible) notion that we need them. Creating objects that are not driven by a need, driven by actual code trying to call them, inevitably results in objects that don’t quite fit the need. When our guesses are good, the fit will be pretty good. Where our guesses miss the mark, the objects will be a bit awkward to use.
So far so good with these, but we’re not done yet.
Another way of writing the program would have been to write it out a bit more longhand. Imagine something like this:
We decide we want a report of classes, each with its method count and methods. And we decide that we need to scan all the tabs, once only, to be sure that we have all the information, since it is quite possible to put methods for a class in tabs other than the tab that defines the class. It’s perhaps evil, but it’s possible.
So we start writing. We’d create the match patterns along the way, much the same, and we’d run the tabs by them, and let each one do the match thing. We could even save the match returns and iterate them more than once. There’s no rule against that.
So we’ve got two gmatch over all tab results, one with class definition captures and one with method definition captures.
We could then loop over the class ones, and there’s the class and subclass name, and we’d put those in our output, and then we’d grab the method captures and search them for the ones for our class name, and save those up, and count them, and then (we’re not there yet) print them after the class.
We’d have a much more “open face” program than we have now. The loop would get larger and larger, and so we might extract some functions and methods, and maybe some objects.
Where might we end up? I frankly doubt that we’d end up with objects just like we have now. I think the program would be more “flat”, more “open face” even when “done”. But it could still be quite a nice program.
With fewer objects, the flow would be embedded in the open face, so if we wanted just a class report, or we wanted methods in another order, we might wind up inserting ifs and elses and whatnot. We could wind up with duplicated code, which we might identify and reduce. We’d likely be able to come up with a report that our open face program would have trouble with, and we’d likely have to go through it and extract bits and pieces to build another version for that report.
Then we’d paste, add more ifs, and so on.
But until we got to that bad part, we’d only have the absolute minimum code we needed to do the job. It’s an interesting enough option that maybe we should do it to see what would actually happen.
Maybe. Speculation about speculation. We’re here now.
Returning value accumulators
I have a very poor record with variables that accumulate values whether they are sums or long strings. I almost always remember to declare the variable, but then I code the accumulation stuff, and then I forget to return it.
What could I do to avoid this error at least once in a while? I can’t fire myself: the cat needs me at breakfast time.
The error usually gets caught quickly. One possibility is just to rely on that fact. But today, I did it twice, at two levels, so that to make one test run I had to add two different returns. The second one surprised me and was a bit harder to find. Just a bit.
I think I’ll try to do two things.
First, I’ll try always to write the return first:
function someMethod()
return result
end
If the editor had templates, I’d use that template for new methods. I actually did that in Smalltalk, changing the new method text to include a return, because I’ve been making this mistake for decades. But Codea does not let me do that. I can try to learn the new habit, which should help.
Maybe there’s a way to use the iPadOS Shortcuts to do this, but I am doubtful. I’ll look into that.
The other thing would be to use “fake it till you make it” more often. Usually I create a test that looks for a real answer, and then code real code to get the answer. If I were to write a test for a trivial case that needs a literal answer, maybe I would be more likely to do it right.
But I think today I actually did return a fixed result and then when I extended that method, I made the mistake.
I’ll think further about this. There might be some really clever way of tricking myself into not making this common mistake. I think it’s always worth exploring how to avoid common errors that one makes, whether they’re typos, common coding errors, or mistaken sword attacks on what turns out just to be your neighbor. Sorry again, Brad.
All that aside, We have a nice structure in place for our full report, delegating downward. I’m not sure we have all the bits in quite the right place, as format knowledge is spread over a few classes, but the code is nicely compact and generally defers decisions to the object with the knowhow.
We’ll see what happens next. See you then, I hope!