XST 30: Exploring, Refactoring, Mistakes.
I have in mind small things for today, starting with an interesting and confusing mistake left over from yesterday.
Yesterday’s version in the zip file had an interesting mistake. Right after the correct implementation of a method, there was an empty definition of the same method. The tests did not run. Somehow this means that I must have started a refactoring for that method, then decided that I couldn’t do it, and left the shell there. I often create a second method of the same name when I’m doing a new version of a method, to give me a convenient reference to the one that works.
Sure enough, we closed yesterday with me deciding that I couldn’t use scopeSet
in getScopeSet
, even after I had changed the definition of scopeSet
to work well with the statistics capability. And then I didn’t run the tests one more time before shipping.
I’ve shipped the good version already, so if you’re just a bit behind on downloading the Codea Lua source that you probably have no use for, well, you’re OK now.
I am no longer confused: I see in the captain’s log where I made the mistake. Since Codea is what it is, I think all I can do is be more careful. I know of no good way to cause exporting a Codea project to run its tests first. In fact, I know of no way at all. I don’t think iOS Shortcuts is up to that, but I’ll double check. Later.
I found the problem last night, on my other iPad, where I was browsing the code and playing with the idea of making Lua tables act like sets, and with giving them the support functions we have on sets, like exists
and every
. There are at least two ways we could do that last bit.
In my browsing, I found an interesting mistake.
Mistake
Recall that XSet class is a provider of general set methods, and that it relies on the XSet instance’s data
member to be an object that can at least do hasAt
and elements
, which is sufficient to allow al the set operations to run.
I think it’s interesting how closely this adheres to mathematical set theory. In set theory, everything about set operations is defined in terms of the null set, and the element
operation, which is essentially a query: does this element exist?
Everything else is defined in terms of element, with a kind of a glitch. Suppose we wanted to define the notion that Y is a subset of X. In math we’d say something like this:
Set Y is a subset of Set X iff for every y element of Y, y is an element of X.
Mathematically, the “for every” is a mathematical notion and doesn’t imply that we can enumerate the set. (In fact, set theory works fairly well on sets that literally cannot be enumerated.) But here on our computer, we need to check that the condition is true for every element. So we need the elements
iterator to allow us to implement a computer kind of set theory.
So that’s neat: we come down to the same simple notions at the core of our implementation as we’d find in the math. I take that as a clue that there’s something good about the design.
But I digress. We’re here to talk about a mistake. When we create an XSet,
it creates the instance with an XGeneric
as its data
member. This is the bottom-level set implementation that we get if we don’t do something special. But we can do things that are special.
We have the CSVSet
, which is a set that allows us to process a file of CSV lines as if it were a set. That’s pretty nifty and I’m proud of it. But there’s a mistake. Patience, we’re almost there.
To create an XSet
with something other than an XGeneric
data element, we use XSet:on(aDataSet)
. It looks like this:
function XSet:on(aDataSet)
self.data = XGeneric()
local result = XSet()
result.data = aDataSet
return result
end
on
is used as a class method. self
, in this code, refers to the XSet
class. So setting its data element doesn’t make sense. It’s harmless, but it’s wrong. We can and should remove it.
I’ve changed, tested, and committed both the changes mentioned here so far. Reviewing the XStatistics
class, there’s still stuff not to like.
function XStatistics:getScopeSet()
local groupFields = self.control.group or {}
local scopeSet = XSet()
for i,field in ipairs(groupFields) do
scopeSet:addAt(field,field)
end
return scopeSet
end
We could inline the local:
function XStatistics:getScopeSet()
local scopeSet = XSet()
for i,field in ipairs(self.control.group or {}) do
scopeSet:addAt(field,field)
end
return scopeSet
end
That puts a rather complex expression inside the ipairs
, and I’m not sure that I really like it. I don’t think I’ve done that before, and I’m sure I don’t do it often. I think we’ll not do that after all. I tried it on, it didn’t fit.
What we have going on here is in essence a reduce
function, but it’s operating on a table rather than a set, so we can’t use our XSet:reduce
. But we could … if tables were sets.
This is a bit incestuous, but in Lua we do have very nice table handling, and we can express both arrays (tuples) and hashed tables, which are a lot like records.
Let’s see if we can write some tests and make tables act like simple sets.
We already have a decent array implementation in Tuple
, so let’s see about something for arrays that look like this:
{ last="Jeffries",first="Ron",city="Pinckney"}
_:test("Table as XSet", function()
local tab = {"a",x=5, "b","c","d",last="Jeffries"}
local set = XSet:fromTable(tab)
_:expect(set:at("1")).is("a")
_:expect(set:at("x")).is("5")
_:expect(set:at("2")).is("b")
_:expect(set:at("3")).is("c")
_:expect(set:at("4")).is("d")
_:expect(set:at("last")).is("Jeffries")
end)
Here I’ve made some decisions. First, the method that does the job will be called fromTable
. Second, all the keys will be strings, even if they appear as numbers in the original table.
Test should fail looking for fromTable
.
39: Table as XSet -- Tests:516: attempt to call a nil value (method 'fromTable')
Now the class method:
function XSet:fromTable(tab)
return XSet:on(XGeneric:fromTable(tab))
end
We want a generic XSet here, I think. So we’ll leave it up to XGeneric to create the instance. Now should fail asking for XGeneric to understand.
Ow. Stack overflow. XGeneric inherits from XData which inherits from XSet. I had forgotten that little trick. Remind me to think more deeply about that. Later. Anyway we can implement fromTable
on XGeneric:
function XGeneric:fromTable(tab)
local xg = XGeneric()
for scope,element in pairs(tab) do
xg:addAt(tostring(element),tostring(scope))
end
return xg
end
OK, that’s nice. Now we can make simple sets more easily.
I think we can commit this: XSet:fromTable converts tables to XSets with string element and scope.
It may be worth noting that, as written, we can’t use this function to create a set with a set as an element: the tostring
will just dump whatever the print string is for an XSet:
function XSet:__tostring()
if self:isNull() then return "NULL" end
return "XSet("..self:card().." elements)"
end
Let’s improve the test and change the rules that we’ll convert numbers to string but everything else is left alone. That will allow something odd: it will allow a set to contain a table. We may choose to deal with that, but why not allow it.
_:test("Table as XSet allows XSet elements", function()
local s1 = XSet():addAt("first","Ron"):addAt("last","Jeffries")
local s2 = Tuple{10,20,30}
local tab = {"hello",person=s1,tens=s2}
local set = XSet:fromTable(tab)
_:expect(set:at("1")).is("hello")
_:expect(set:at("person"), "person set").is(s1)
_:expect(set:at("tens"), "tens set").is(s2)
end)
This should fail not matching person set and tens set.
40: Table as XSet allows XSet elements person set -- Actual: XSet(2 elements), Expected: XSet(2 elements)
40: Table as XSet allows XSet elements tens set -- Actual: XData???, Expected: XData???
The second message tells me that we could do better with the print string on a tuple. Let’s make the test run.
function XGeneric:fromTable(tab)
local stringify = function(e)
return type(e)=="number" and tostring(e) or e
end
local xg = XGeneric()
for scope,element in pairs(tab) do
xg:addAt(stringify(element),stringify(scope))
end
return xg
end
That’s rather nice if I do say so myself. Test runs. Commit: fromTable
only converts numbers to strings, all else is accepted as is.
Now about that print string. What happened there?
I find this:
function XSet:__tostring()
if self:isNull() then return "NULL" end
return "XSet("..self:card().." elements)"
end
That makes me think that the display should not have returned what it did. I want to see that error again. I’ll break the test:
_:expect(set:at("tens"), "tens set").is(36)
That gives me this:
40: Table as XSet allows XSet elements tens set -- Actual: XData???, Expected: 36
What the heck is a Tuple anyway?
Tuple = class(XData)
Now it happens that XData inherits from XSet so apparently this works but shouldn’t we really be creating an XSet containing a Tuple?
A review of usage tells me that no, Tuple is a stand-alone thing that acts like a set. I’m not sure I’m comfortable with that from a design viewpoint, but we can just add a tostring
and should be happy.
function Tuple:__tostring()
return "Tuple 1-"..self:card()
end
Now the message is:
40: Table as XSet allows XSet elements tens set -- Actual: Tuple 1-3, Expected: 36
Fix the test:
_:test("Table as XSet allows XSet elements", function()
local s1 = XSet():addAt("first","Ron"):addAt("last","Jeffries")
local s2 = Tuple{10,20,30}
local tab = {"hello",person=s1,tens=s2}
local set = XSet:fromTable(tab)
_:expect(set:at("1")).is("hello")
_:expect(set:at("person"), "person set").is(s1)
_:expect(set:at("tens"), "tens set").is(s2)
end)
Green. Commit: Tuple has reasonable tostring.
However
My motivation here was to clean up this method:
function XStatistics:getScopeSet()
local groupFields = self.control.group or {}
local scopeSet = XSet()
for i,field in ipairs(groupFields) do
scopeSet:addAt(field,field)
end
return scopeSet
end
I wonder if I can do any better here now. Let me try this:
function XStatistics:getScopeSet()
local groupFields = XSet:fromTable(self.control.group or {})
return groupFields:reduce(XSet(), function(r,e,s)
return r:addAt(e,e)
end)
end
The tests all run. We can commit this. Is it better than what we had before? Well, if one is a wizard with reduce
, it probably is, and if not, probably not.
This verges on too clever to live, but it’s my darling and I’m not going to kill it. Commit: XStatistics:getScopeSet uses reduce
to produce the set.
Surprise Summary
At this point in the narrative, I decided to see whether I could work out how to “view” a table as an XSet. This led me down a primrose path of setting metatables and, at this point, I don’t quite see how to get what I want. I’ll do some kind of spike offline and then come back to this idea. I promise to report on what happens, but I don’t think it’ll be productive for you to watch me fumbling along with something I don’t understand.
That aside, we have two little fixes, and we have the ability to quickly convert a table to an XSet, which we used to good effect in getScopeSet
. Well, I call it good effect. If you disagree, let me know. We can discuss whether and when things like reduce
are best used.
I’m still inclined to provide those functions as part of table
, but if instead I can make tables act like sets, I’ll get the same capability more in tune with the current application.
I hope you’re enjoying these odd articles. If not, please let me know. I have other odd ideas.
See you next time, I hope!