This morning, I plan to add detailed method reporting to the full report. I am expecting no real trouble but have recognized a mismatch.

Before I forget, the mismatch is that the report is generated as a single string (with newlines), but (I believe) Codea’s text display will not display anything if the next extends beyond the screen limits. If this is the case, then the display logic will want an array of lines, not a single string.

It should be clear that this will not be a big deal. We can readily create an array of lines from a big string, and just as readily, we can change the reporting to produce an array of lines directly.

However, just yesterday I mentioned that there’s a common difficulty in building a program bottom up, speculating about what useful objects might be. Often, when it comes to using the object, we find that they don’t really quite fit the need. Common advice given in my community is to build the application in “thin vertical slices”, by which we mean to go right from bottom to top in every step, from the raw data to the displayed output and behavior.

I didn’t do that here, and even in a tiny program like this, we see what happens.

Amazing how small examples are like reality. Can you think why that might be? I have some ideas that I mention from time to time.

Anyway, today I think we’ll just enhance the report in its single string style and then move along to whatever’s next.

Method Description Output

Our current full report looks like this:

Classes

    SampleCode (4)
    SubclassOfSample(SampleCode) (1)

We have another test that returns all the methods in our sample:

    SampleCode:init(x)
    SampleCode:draw()
    SampleCode:touched(touch)
    SubclassOfSample:init(ignored)
    SampleCode:extraMethod(a,b,c)

Now what I have in mind for the “full report” is that after each class is identified, we’ll list the methods for that class, tabbed in appropriately. I see no reason to show the full name SampleCode:init(x). It seems to me that just init(x) will be just fine in context. Since I have the strings here in hand, I’ll enhance the full report test.

        _:test("ProjectDescription fullReport", function()
            local desiredReport = [[
Classes

    SampleCode (4)
        init(x)
        draw()
        touched(touch)
        extraMethod(a,b,c)
    SubclassOfSample(SampleCode) (1)
        init(ignored)
]]
            local report = ProjectDescription("SampleStatsProject"):fullReport()
            _:expect(report).is(desiredReport)
        end)

We know this will fail, and we know the message will be hard to understand:

5: ProjectDescription fullReport  -- Actual: Classes

    SampleCode (4)
    SubclassOfSample(SampleCode) (1)
, Expected: Classes

    SampleCode (4)
        init(x)
        draw()
        touched(touch)
        extraMethod(a,b,c)
    SubclassOfSample(SampleCode) (1)
        init(ignored)

I note in passing that if the output had been an array of lines, we could have readily produced errors down to the line, and with a little more work, down to the character. We still could do that, even if the report were to remain a long string.

Something something about “the natural form of input and output”. Possibly big long strings are not “the natural form”.

Anyway, we need to enhance the full report to get this additional information, and I expect that to be pretty easy.

Our full report calls down to the class collection, passing the full method collection:

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

That defers to each of the class definitions:

function ClassDefinition:fullReport(methodCollection)
    local result = self:report()
    local count = methodCollection:countForClass(self.className)
    result = result.." ("..count..")\n"
    return result
end

And here, we are in a position to get the method information. Let’s do a simple refactoring first, to get the method collection pertaining to this class. Currently we just use the countForClass method, which hides the refined method collection.

function ClassDefinition:fullReport(methodCollection)
    local result = self:report()
    local myMethods = methodCollection:methodsForClass(self.className)
    local count = myMethods:count()
    result = result.." ("..count..")\n"
    return result
end

Here, we got the method collection to give us the one pertaining to the current class name, and counted it. So in the test, I expect the counts to remain good, and they do. But now, we can do this:

function ClassDefinition:fullReport(methodCollection)
    local result = self:report()
    local myMethods = methodCollection:methodsForClass(self.className)
    local count = myMethods:count()
    result = result.." ("..count..")\n"
    result = result..myMethods:fullReport()
    return result
end

There is no such method on method collection. We confirm with the tests:

5: ProjectDescription fullReport -- TabTests:192: attempt to call a nil value (method 'fullReport')

Easily rectified:

function MethodCollection:fullReport()
    local tab = "        "
    local result = ""
    for _i,meth in ipairs(self.methodDefinitions) do
        result = result..tab..meth:fullReport()
    end
    return result
end

If we test now, we get a different complaint about full report, because MethodDefinition doesn’t have one yet.

5: ProjectDescription fullReport -- TabTests:165: attempt to call a nil value (method 'fullReport')

We provide that:

function MethodDefinition:fullReport()
    return self.methodName.."("..(self.args or "")..")"
end

I cribbed for that from this method:

function MethodDefinition:report()
    return self.className..":"..self.methodName.."("..(self.args or "")..")"
end

There’s some duplication there that we could us. And it’s interesting that the output for “fullReport” is shorter than that for “report”. Maybe we need better names. We usually do.

I think this might work. Let’s find out why I’m wrong. The message is messy but informative:

5: ProjectDescription fullReport  -- Actual: Classes

    SampleCode (4)
        init(x)        draw()        touched(touch)        extraMethod(a,b,c)    SubclassOfSample(SampleCode) (1)
        init(ignored), Expected: Classes

    SampleCode (4)
        init(x)
        draw()
        touched(touch)
        extraMethod(a,b,c)
    SubclassOfSample(SampleCode) (1)
        init(ignored)

The programmer forgot the newlines. Another good reason for an array of lines. Easily fixed:

function MethodDefinition:fullReport()
    return self.methodName.."("..(self.args or "")..")\n"
end

Now then:

5: ProjectDescription fullReport  -- OK

We have our report. Commit: fullReport now includes methods for each class.

Reflection

There’s no doubt that that was easy: even I was able to do it! But there are issues that I noticed along the way.

There’s sort of random insertion of tabs and newlines here and there, and it’s not even fully consistent. We may find that that sorts out when we get to using an array of lines, but in any case it’s working. The thing is, when we do tabbing and newlining in an ad-hoc fashion like this, things just get more and more strange as we create more reports. We’d like to reuse the fullReport for the method, so we have to remove the newline but if we use the regular report, it has no newline.

Many of you have probably found yourselves tweaking formatting all over the code, just to get something to display correctly. No? Well, I certainly have. I wish you had been her to help me.

What Now?

I have maybe a half hour before Sunday breakfast and Sunday morning programs, so it’ll have to be quick, but I really want to see what the D2 Dung program looks like. Let’s see if we can hack something into Main to do that.

A quick and dirty hack:

function setup()
    if CodeaUnit then 
        codeaTestsVisible(true)
        runCodeaUnitTests() 
    end
    dungPD = ProjectDescription("D2")
    local rep = dungPD:fullReport()
    repLines = {}
    for line in rep:gmatch("(.-)\n") do
        print(line)
        table.insert(repLines,line)
    end
end

function draw()
    --if CodeaUnit then showCodeaUnitTests() end
    pushStyle()
    local y = HEIGHT
    stroke(255)
    fill(255)
    textAlign(LEFT)
    textMode(CORNER)
    for _i,line in ipairs(repLines) do
        text(line, 300, y)
        y = y - 20
    end
    popStyle()
end

This gives me an almost credible report:

report

A thing that I do not understand is why it doesn’t say “Classes” at the top, and that’s why there’s a print in the setup: I wanted to see if it was there. And it is. I suspect the issue is that the corner of the text is lower left not upper right.

Yes, Setting the starting y to HEIGHT-20 allows the word Classes to appear. Well done, Ron.

Commit: program hacked to display some D2 classes and methods.

Let’s sum up.

Summary

Today went well. Why? Because while they aren’t producing quite what we want, our objects are really helping us. The Class and Method Collection objects know how to report, by delegating to their contents, and the contents have a couple of custom reporting methods to meet our various needs. And the MethodCollection, in particular, has the convenient ability to return all the methods for a given class, as a MethodCollection.

I think I’d like things better if the tabbing and newlining (if we even wind up with any) were handled in some more centralized way. We could even imagine display-oriented methods, instead of string- or array-oriented, right in the Class and Method classes.

I am concerned about that idea, though, because as a rule, one wants to have the “view” separate from the “model”, and pushing display logic clear down into the Description objects is probably wrong. It would also probably work, but it would be inflexible.

We’ll see about that when we work a bit further. I think we need to keep in mind that our purpose here is to learn a bit about our “shipping app”, as GeePaw Hill calls it. We’re not trying to write a fully polished Codea Project Analysis Tool. That might be fun, but it’s not our mission.

If we even had a mission.

See you next time!