Today let’s start on some small classes for Sidings and maybe Cars. (Things do not go quite as I planned. Nothing new there.)
Yesterday, we got a slightly improved testing setup in place, and verified that swapping adjacent pairs into numeric order serves to sort the whole train in due time. I remembered the name of the sort we emulated: Bubble Sort. Famously inefficient, but at size five no one cares, and once we start moving train cars around, we won’t really be sorting a table anyway.
Today, what I’ll be working toward is a scheme something like this:
There’s at least one other object that we’ll probably need, the one that figures out what to do next and controls the action. It’s too soon for me to say what that is. We’ll try things, move things around, find out what seems right.
I plan to do as much of this as I can by first writing a small test for the next thing we need, and then making the test run. Since all our tests run every time we save the file, we should be pretty confident that everything is still OK as we go.
Here’s the first Siding test:
function Tests:test_first_siding()
siding = Siding:new(3)
assert_equals(siding.length, 3)
end
I find and adapt my rudimentary class-creation code for a Siding class:
Siding = {}
function Siding:new(length)
obj = {}
self.__index = self
setmetatable(obj, self)
obj.length = length
return obj
end
To review, the above code creates a table Siding
, which serves as our class definition. It has, so far, a single method new
, which creates a table obj
… oh, my, that should be local … fixed. Anyway, we then set Siding.__index
to Siding, because when when we call new
, Siding
is self
. Then we set the new object’s metatable to Siding, which means that, because __index
is the Siding
table, the object will look for functions called on it in Siding
. So the object instance methods will be defined in Siding
.
I think that code would be more clear like this:
Siding = {}
function Siding:new(length)
local instance = {}
self.__index = self
setmetatable(instance, self)
instance.length = length
return instance
end
Make it so.
My test has run correctly three times now, each time I saved the file as I wrote this.
obj
variable instance
seemed better to me. I have tests of everything running, so I can just make the change and save the file and the tests tell me instantly whether the change is OK. If it isn’t, I either Command+Z the change out, or fix the problem. Usually, it’s just fine, and the code gets a bit better.OK, what next? I’m not sure, but it seems like the ability to push cars onto the siding will be a good idea. And that brings up a thought.
Yesterday, our simple lists just contained integers that started out not in order and that we swapped around to put them in order. What should we put into our Siding instances? We could just use integers … but looking forward, it seems likely that a Siding will contain railroad cars, perhaps called Cars
. Should we work toward that right now, creating a new Car class and putting them into Sidings? Or should we just put numbers in there?
Having thought about it, I am inclined to create a trivial class Car and put it in there. When we finally build a Second Life object that displays the shunting, there will probably be cars with coordinates in the sidings.
My experience is that things go best for me if I move away from simple types as soon as possible, so let’s do a quick Car class. Cars will have a number, I think, or an identifier, one through five or perhaps “blank” or “x” or something for the cars that the game just puts in there to make our job harder. Let’s go for a string id
.
I’ll write a little test, just to get the ball rolling.
function Tests:test_car_id()
car_1 = Car:new("1")
car_irritating = Car:new("x")
assert_equals(car_1.id, "1")
assert_equals(car_irritating.id, "x")
end
And that won’t work without this:
local Car = {}
function Car:new(length)
local instance = {}
self.__index = self
setmetatable(instance, self)
instance.length = length
return instance
end
(I also changed the other class, Siding
to be local. I’m told that’s the better thing to do, that globals are costly.)
Save expecting 8 tests to run. They do not. Did you see my mistake, due to the copy-paste?
[05:53] Inglenook 0.2: false == nil false
[05:53] Inglenook 0.2: false == nil false
[05:53] Inglenook 0.2: Tests: correct 7, wrong 2
Right. I should have said:
local Car = {}
function Car:new(id)
local instance = {}
self.__index = self
setmetatable(instance, self)
instance.id = id
return instance
end
I did make another mistake here. I wrote
assert_equals(car_1.id == "1")
That’s not going to work. It would work in Python using pytest, which is why my habits got the better of me. Anyway, we’re good now, with 9 tests running.
Of those 9, only two have to do with sidings and cars. I have a copy of this script in an older object, and if I could use git more readily with SL, I’d have the code in git. The other tests, from yesterday, just verified that checking for inversions and reversing them sorts the sequence, no big surprise, so I’m going to remove all that from the current version of the script. When I’m done, we should have just 3 asserts running, not 9, but the code will be more focused.
Done.
Wow, writing down my every thought creates a lot of words, doesn’t it? Well, it is my fashion.
Thanks for asking. Next, I think we’ll test and build the ability to push cars into a siding. Let’s have a method push
that is given a car and if the siding has room for it, the siding adds it to itself, and returns true
, otherwise returning false
. And let’s have the Siding able to tell us what cars it has in it. Maybe just as a list?
I’m not sure what methods we’ll need after push
. I’d prefer not to guess. Instead we’ll try to start using the Siding and Car objects in the same way our program that solves the puzzle will use them. We’re kind of devising the “language” of cars and sidings and shunting.
Anyway first push
:
function Tests:test_push()
car = Car("1")
siding = Siding(2)
assert_equals(siding:push(car), true)
assert_equals(siding:push(car), true)
assert_equals(siding:push(car), false)
end
We’ll never really push the same car twice, and Sidings don’t care, so we’ll just use the car over and over. Since the Siding length is 2, we should be able to push two, but not three cars into it.
We need Siding:push
:
function Siging:push(car)
if #self.cars == self.length then
return false
else
table.insert(self.cars, car, 1)
return true
end
end
As soon as I write that, I realize that I want to push separate cars into the Siding in my test, so that I can check that they’re in the order desired, last in first out. Adjust the test even before running:
function Tests:test_push()
siding = Siding(2)
assert_equals(siding:push(Car("1")), true)
assert_equals(siding:push(Car("2")), true)
assert_equals(siding:push(Car("3")), false)
end
I do expect this to run when I save … but I forgot that I’ve not implemented the cars
part. The test reminds me.
local Siding = {}
function Siding:new(length)
local instance = {}
self.__index = self
setmetatable(instance, self)
instance.length = length
instance.cars = {} -- added
return instance
end
Oh the test was telling me that I had put a typo in Siding:push
, calling it Siging
. Not so good. Fix that.
Now I get a new error:
lua_script:89: attempt to call a table value
lua_script:89 function test_push
lua_script:70 function run_tests
lua_script:33 function state_entry
lua_script:96
Right, I’m not creating the class correctly! This is why I test, and this is why I want a better way of defining a class. The test should call new
:
function Tests:test_push()
siding = Siding:new(2)
assert_equals(siding:push(Car("1")), true)
assert_equals(siding:push(Car("2")), true)
assert_equals(siding:push(Car("3")), false)
end
Now this baby better run! Oh bah! I have to use new
on the cars as well:
function Tests:test_push()
siding = Siding:new(2)
assert_equals(siding:push(Car:new("1")), true)
assert_equals(siding:push(Car:new("2")), true)
assert_equals(siding:push(Car:new("3")), false)
end
Now? No. Arrgh!
lua_script:27: invalid argument #2 to 'insert' (number expected, got table)
[C] function insert
I’d better look up table.insert
, I must have the arguments wrong in my mind. Right, the index goes in the middle. Truly weird.
function Siding:push(car)
if #self.cars == self.length then
return false
else
table.insert(self.cars, 1, car)
return true
end
end
My tests are all running:
[06:32] Inglenook 0.2: Tests: correct 6, wrong 0
This article is well long enough. Let me summarize, because what has happened here is not quite what I expected, but it is important.
Yes, saved me. If I were working on a large program and was just creating and modifying these objects in the middle of it, I would not save so often, I would not run so often, and when things broke, I would not know where to look. It would take me longer to find out that I was off track.
Here, my tests allow me to choose very tiny things to do, and, as we saw here this morning, even doing tiny things, I can make mistakes. I’ll talk about the specific mistakes a bit below, but the truly important thing is that with tiny tests, I make tiny changes and my errors get caught instantly, because I can and do save very frequently, and my tests run on ever save.
When the tests run, I can be very confident that things are working as I intend, even after a series of mistakes like I made this morning. Instead of things being broken making me lose confidence, my tests let me identify specific failures and I can do the specific fixes needed. That leaves me confident that everything I’ve asked the program to do, it really does.
What about the specific errors? Is there something to learn?
I forgot to say, for example Car:new("x")
, instead saying Car("x")
. Why? Because that’s what I’d say in Python or almost any other reasonable language. My way of defining classes needs improvement. Because I’m devising the “language” for writing this program, and because I’m rolling my own classes, I can find or create a better way of defining them. And I do have such a thing tucked away somewhere. I’ll put it on my list to do something about this.
We might possibly be able to fix the issue with a single entry in the class table, which might be worth a try, because the more capable class definition function I have is longer and probably has things in it that we will not usually need. We’ll see.
I got the table.insert
wrong. I don’t see offhand anything that I can do other than try to remember how it works. Honestly I think I’d have put the optional index on the end of the call if I were creating Lua. Anyway the mistake was quickly identified.
I had a typo in the name of the class when defining a method, Siging:push
instead of Siding:push
. Lua is quite willing to add a function to nil, apparently, so our first detection of that mistake will be the first time we use push
. Another good reason to write little tests first, and a good reason not to write methods before we need them.
So, one item for future consideration, a better class definition scheme. And a nod of thanks to the mentors who beat the notion of small steps and test-driving the code into my pretty little head.
Until next time: safe paths!