I begin to see how I really want this thing to work, where “really” is a bit iffy. I’m at a point where I begin to see the simplicity that I like to find when I solve a problem. Let’s discuss this.
You may recall that the current code, with the tiny Siding and Car and Locomotive classes, is the second design we’ve had. The first scheme was pretty much just a table with a list of integers and a function or two to swap them around. That little example convinced me that we could solve the shunting problem if we could just accomplish two things: come up with a pattern for swapping any two adjacent cars in the train, and come up with a way to get all the numbered cars in the 5-siding and the blocking cars in the 3-sidings.
Since then, we’ve been playing with objects, building up to the current Car, Siding, Locomotive classes, with various tests showing that they all do whatever it was that we (OK, I) wanted them to do. I just don’t like saying “I” all the time. “We” feels more inclusive to me. Anyway …
I think of this part of a design as inventing a little language for the domain, in this case the domain of shunting railroad cars in a very simplistic situation. A typical language will have nouns and verbs and maybe other kinds of words. Our objects are beginning to provide nouns, car, locomotive, siding, and verbs, move, pull, push, take, drop, whatever we wind up picking.
Yesterday, the key test began to demonstrate a very simple language:
function Tests:test_pull_one_car()
siding5 = Siding(5, "21345")
siding31 = Siding(3)
siding32 = Siding(3)
loco = Locomotive(siding5, siding31, siding32)
plan = loco:fetch(2, 1)
assert_equals(plan, "m2 p1 mh", "check plan")
end
In the test above, the language has verbs m
and p
, which are things the locomotive presumably understands and wants to do, and nouns 2 and h, representing the position to move to, and 1, indicating the number of cars to pull out from the siding 1.
That’s pretty cryptic. It was invented to make writing tests easier, because having lots of small tests always working is key to the way that I like to work. When I have enough tests (and it doesn’t really take all that many) and they all work, I can be pretty confident that the program is working as I intend. If it isn’t working as I intend … that’s a clear sign that I’m missing at least one test, since something has gone wrong and no test detected the issue. So, on my best days, I write that test.
Writing tests is just about the least fun, for me, in all of programming, so I try to make it as easy as I can, because the least fun, for me, in all of programming, is having a defect in the program and not enough tests to help isolate it.
Now when we read that “plan”, “m2 p1 mh”, we know that it really means something more like “move to siding 1, pull out one car, move home”. That’s certainly more readable, but not all that easy to write and format. And it’s harder to deal with if we were going to actually do that plan. We’d have to parse it apart and then look up “move to siding” or whatever. That output is pretty good for reading, a bit of a pain for writing, more of a pain for typing in, and it’s harder to make the program understand the long form than the simple “m1” kind of thing.
I was just getting to that. There are quite a few steps that we might take with this exercise, including:
As for numbers 3 and 4, they have to wait until #1 and #2 are well in hand. Definitely future. It would be nice if what we build supported moving objects around, but I’m not terribly worried about that: my work in SL is mostly about moving objects around.
I would say we are doing well on #1. We’ve begun to use classes and objects, we have a nice little testing framework growing, and, believe me, I’m making lots of errors and learning my way around SLua.
As for #2, I think we’re getting closer to a reasonable thing to do, and we’ll focus on that now. This, in fact, is my point.
The test above, as written, suggests that the Locomotive has a sort of two-phase operation: make a plan to improve the situation, and then execute the plan. Repeat until the situation is satisfactory. The string “m2 p1 mh” is an example of a report on the plan, and may or may not reflect the Locomotive’s internal view of the plan. We haven’t really related the Loco’s siding members and their contained cars to the Loco’s model of the situation.
That needs to be done. Remember that the current test, shown above, only passes because Locomotive fakes the answer:
function Locomotive:fetch(siding, count)
return `m{siding} p{count} mh`
end
The Loco didn’t even decide to fetch
: the test calls it. That said, I think that “fetch count cars from siding” is a pretty good candidate for a primitive operation that the Locomotive might need. Candidate, mind you. As we build up decision logic we’ll find out what we really need.
What I think we’ll try is this:
Something like that. One larger comment: as we do this, we should expect that all our objects will need adjustment or perhaps even replacement. Our little language may even change—in fact I’m sure it will. That’s fine, GeePaw Hill{.target=”blank”} reminds us that we are in the business of changing code.
Let’s get started.
I’d like to get to the point of actually changing what the Loco knows, that is, to the point of executing a command.
Oh, that reminds me, I was thinking that we could represent the whole state of the game with a small table or string, roughly like this:
train | 5-siding | 3-siding 1 | 3-siding 2 |
---|---|---|---|
L15 | 234 | xx | x |
I’m thinking that we’ll need at least a few tests of starting and final positions, and if so, we might use some kind of string representation of those four values.
In principle, if we know the sequence to invert any pair in the 5-siding, our whole program just consists of a series of calls to invert(x, x+1)
. But that’s not fine-grained enough: it can take several moves to do a given inversion. I think we want to represent much like our test above suggests: start from loco home, possibly even with loco empty. move to 5-siding, get some cars, move to 3-siding 1, drop 2, move to 5-siding, drop 1, move to 3-siding 1, take 2, move to 5-siding, drop 2, move home.
So I think our fundamental operations are move, take, drop, and the move targets are the home siding and the three working sidings.
Let’s test and build take and drop. I think that move will be different from those, more a matter of identifying what siding we’re at.
function Test:test_take()
siding5 = Siding(5, "21345")
siding31 = Siding(3)
siding32 = Siding(3)
loco = Locomotive(siding5, siding31, siding32)
loco.take(1, 2)
assert_equals(loco.car_string(), "21")
assert_equals(siding5.car_string(), "345")
end
I’m supposing that take
uses the siding number 1, 2, 3, not the object. I have a feeling that the Locomotive code should determine what the internal representation is and I don’t want to tie it too tightly to the current Siding idea. In fact … maybe we should ask the Loco about the siding contents? Yes, let’s do that. Change the test:
function Tests:test_take()
siding5 = Siding(5, "21345")
siding31 = Siding(3)
siding32 = Siding(3)
loco = Locomotive(siding5, siding31, siding32)
loco:take(1, 2)
assert_equals(loco:car_string(), "21")
assert_equals(locodsiding_car_string(1), "345")
end
Now a bit of work on Locomotive.
function Locomotive:take(siding_number, count)
local siding = self:get_siding(siding_number)
local taken = siding:pull(count)
self:append(taken)
end
Taking a few cars from a siding involves finding the siding by its number, since currently they are separate member variables. Then we pull count
cars from the siding. The pull
method updates the siding and returns the cars pulled. We append those to ourself, the Locomotive.
function Locomotive:get_siding(siding_number)
local ss = {self.s5, self.s31, self.s32}
return ss[siding_number]
end
This method covers the fact that our sidings are currently separate members. We create an array of them and then return the appropriate one. This can and should be done differently but I followed a very important rule, the rule of hats, which I first heard from Kent Beck, something like this:
You have two hats, an implementing hat and a refactoring hat. It is important to only wear one hat at a time.
So I implemented code to make the test pass, and will improve it later.
function Locomotive:append(cars)
for i, car in cars do
table.insert(self.cars, car)
end
end
I think append
is pretty clear, we just insert all the provided cars into the train, from left to right. You may wonder why I didn’t just write the loop inside the take
method. The reason is that I was taught that a method should either do things or make a decision or run a loop, but only one of those things. In short (haha) I make short methods containing simple loops or conditional decisions, when I can and when I’m at my best.
Here, I wanted to append the cars. So I wrote append
and then implemented it. You’d think that Lua would know how to do this but I don’t think it does. Anyway the Locomotive has what it wants.
function Locomotive:siding_car_string(siding_number)
local siding = self:get_siding(siding_number)
return siding:car_string()
end
This method just gets the siding and asks it for its car string. This allows the Locomotive to deal with the sidings as it wishes. As we go forward, I think we’ll have it create the sidings, rather than be passed them. I’m not sure about that, there are reasons not to do it that way, but for now we leave the option open.
function Locomotive:car_string()
local result = ''
for i, car in self.cars do
result ..= car.id
end
return result
end
Pretty straightforward, just build up a string by looping over the cars. However, I think that if we were to look at Siding, we would find a very similar method:
function Siding:car_string()
local result = ""
for i, car in self.cars do
result ..= car.id
end
return result
end
I do not like that duplication and I would like to get rid of it, but at this moment I don’t see quite how we’d best do it. Probably a tiny class CarCollection or something. Not now, Satan, maybe later.
The test passes: we have actually moved two cars from siding 1 to the loco. However, it didn’t go quite as nicely as just typing in all that code. Curiously, with one exception, the code went in easily. The exception was that apparently you can’t say {x, y z}[2]
, you have to say local a = {x, y, z}; a[2]
, which is a crock, but there you are.
Where I made most of my mistakes was that in almost every case of calling a method on an object, I first wrote object.method()
instead of object:method()
. This is of course bleed-over from Python, my current main language, where we type .
to call a method, not :
. I’ll just have to adapt, or get used to a lot of run-time messages about nil parameters.
I’m not sure what to do about this issue beyond testing carefully. There’s just about no way for Lua to know that you needed a colon there. You might have a vanilla function stored that you’re calling, or some such thing.
I think we’ve done enough this morning, my eyes are tired and I am craving caffeine. In sum …
We have our Locomotive actually representing a real operation, taking cars from a siding and appending them to its train. We have not tested the implementation deeply, and might want to test taking one car from ons siding and one from another, but I’m quite sure that that will work. (I could be wrong, so probably we will test it but I’d bet you a Coke that it’ll work.)
And we have some refactoring to do. But for now, we’ve made a bit of progress and I deserve a treat.
Safe paths!