Robot 32
More on TCP/IP. A realization that might have come sooner, but today’s OK too.
TIR that there’s no need to specialize a callback by providing for an object and a method in the setup. You can always provide just to receive a function, and link up the object in the function. I’ve often done something else, such as provide an object and a method (function) on the object, or provide an object and a method name (string). We can always just pass a function.
Suppose we have an object that does a callback, and that we pass it a function to call:
function O:init(f)
self._f = f
end
function O:callbaclk()
return self._f()
end
If we want to provide a plain function, there it is. Suppose we wish, instead, that O would call a method on some object. Then we can do this:
local f = function() myObj:method() end
local o = O(f)
Similarly, if we wanted, for some reason, to call the method by name:
local f = function() myObj[methodString]() end
local o = O(f)
This is obvious, and if you had said to me at any time, here’s an object that takes just a function, make it do one of the things above, I’ve had known instantly how to do it. But somehow, when building objects with callbacks, I’ve sometimes done a more specialized thing in the object itself, rather than in the provided function.
If you’ve been reading my articles, I know you haven’t ever wondered about that, because you would surely have tweeted me or written me an email about it. Unless you just like to see me do not quite right things. Thanks for that.
Anyway, today I realized that. And it’s a good day to have realized, because that’s what I’m going to do today.
To Work
Our WorldConnection object, despite its name, doesn’t need to be limited to the Robot/World situation. It’s a pretty general server object, at least so far. And I do want it to have callbacks. In the Robot World situation, when a client sends a request, we ned to invoke the World object to deal with it, and to send back the reply. And, in principle, when a client is initiated, we might want a callback then, in case we wanted to initialize something on behalf of that client.
In fact, we might actually need that in Robot World, although since they’re supposed to start with a launch request, we might not need it. In any case we should provide for it.
If a user of our connection object doesn’t provide a function, we should default to do nothing rather than explode. We could require them to provide a callback, but for now I think that’s excessively restrictive, not least because I have some tests that are not providing said functions.
Let’s now set up and test a callback for messages received. Because we’ll be testing a few sequences of client creation and messaging, I think I’d like a callback that appends strings together, so that we can compare the output to expected. Like this:
_:test("client callback", function()
local result = ""
local cb = function(client,msg)
result = result..msg
end
local server = FakeServer()
local wc = WorldConnection(server, cb)
local a = Fake("aaa")
local b = Fake("bbb")
wc:addClient(a)
wc:addClient(b)
b:ready()
wc:processClients()
a:ready()
wc:processClients()
a:ready()
wc:processClients()
_:expect(result).is("bbbaaaaaa")
end)
The callback function cb appends whatever message was received to the local result. We test it at the end. I expect this test to fail, “” vs “bbbaaaaaa”:
3: client callback --
Actual: ,
Expected: bbbaaaaaa
And now let’s improve WorldConnection’s init:
function WorldConnection:init(server, clientCallback)
self._server = server
self._clients = {}
self._clientCallback = clientCallback or function() end
end
Note that I initialize to an empty function if one is not provided. Now to call it:
function WorldConnection:processClients()
for i,c in ipairs(self._clients) do
data,err = c:receive()
if data then
result = data
end
end
end
This method is hacked so as to return a result for a preceding test. We’ll deal with that in a moment. For now:
function WorldConnection:processClients()
for i,c in ipairs(self._clients) do
data,err = c:receive()
if data then
self._clientCallback(client,data)
result = data
end
end
end
I rather expect the test to run. Test. Green. Commit: WorldConnection invokes client callback.
Now about that hacked result thing. We could remove that test entirely. It looks like this:
_:test("client receive", function()
local server = FakeServer()
local wc = WorldConnection(server)
local a = Fake("aaa")
local b = Fake("bbb")
wc:addClient(a)
wc:addClient(b)
_:expect(wc:clientCount()).is(2)
b:ready()
wc:processClients()
_:expect(result).is("bbb")
a:ready()
wc:processClients()
_:expect(result).is("aaa")
end)
That test was just scaffolding prior to the callback, to make the steps smaller. I think we should remove it now. We could change it to provide a different callback, but cui bono? Remove it, and the local at the top of the page. Green. Commit: remove unnecessary test.
Let’s provide for a callback on acceptance as well. I’ll write a different test:
_:test("acceptance callback", function()
local result = ""
local acb = function(client)
result = result..client._msg.." connected "
end
local server = FakeServer()
local wc = WorldConnection(server, nil, acb)
local a = Fake("aaa")
local b = Fake("bbb")
wc:addClient(a)
wc:addClient(b)
_:expect(result).is("aaa connected bbb connected ")
end)
Similar. Note the nil client callback in the WC creation. Test will fail with empty result.
3: acceptance callback --
Actual: ,
Expected: aaa connected bbb connected
And …
function WorldConnection:init(server, clientCallback, acceptCallback)
self._server = server
self._clients = {}
self._clientCallback = clientCallback or function() end
self._acceptCallback = acceptCallback or function() end
end
function WorldConnection:addClient(client)
table.insert(self._clients, client)
self._acceptCallback(client)
end
I expect this to run, although for some reason it seems too easy. Test. Green. Change the expect string just to see it fail.
_:expect(result).is("aaa connected bbb connected")
I removed the trailing space. Test.
3: acceptance callback --
Actual: aaa connected bbb connected ,
Expected: aaa connected bbb connected
Perfect. Put the space back. Green. Commit: WorldConnection invokes connection callback when adding client.
Where Are We?
Well, most importantly, we are at Sunday morning, so it is a good idea to write a short article, especially since my dear wife is already up and watching the Tour. So I’ll wrap this up here.
We now have an object, WorldConnection, that can add clients and process clients, and that provides callbacks for client addition and for client received messages. It does not, as yet, poll for attempted client connections. That will be our next improvement, at which point we’ll probably be ready to hook it to a timer and really talk to it.
I noticed a lot of duplication in the tests, notably of this sequence:
local server = FakeServer()
local wc = WorldConnection(server, nil, acb)
local a = Fake("aaa")
local b = Fake("bbb")
wc:addClient(a)
wc:addClient(b)
We could readily eliminate that noise with a different describe
with different before
and after
. I find that I rarely do that. I tell myself that I prefer to see the setup, but I could be wrong about that. I might just be lazy with bad habits.
Next time, we’ll do that, to see whether it improves things, and to see if we need better habits.
For now, an excellent little bit of progress and a nice short article.
See you next time!