Dungeon 223--I'm Bored.
I’m bored with this program. What shall we do about this?
This has happened to me many times in my life. I’ll get a project so far along, and when it seems that the interesting 10 percent is done, I have no energy for the boring 90 percent. Fortunately, this time, no one is paying me to do this, so I can do as I wish.
One possibility is to make it interesting again, in the sense of the curse “May you live in interesting times”. And we certainly are doing that, aren’t we? But I digress.
Some Radical Twist
One way to make the program interesting might be to come up with some radical twist on what it has to be. Three dimensional or something, I don’t know. The trick there is to pick something interesting but still possible.
Improve the Design
Another thing might be to look at the existing design, which is probably partly pretty good and partly pretty awful, and try to make it more good and less awful. That idea has some appeal to me, in that there are some things I might like to try.
Some Nasty Feature
Another thing might be to devise some feature that would be difficult, but since in the past few cycles we’ve cracked the essence of how to do NPC interaction, it seems to me that we’ve demonstrated that the design is good enough to support any reasonable feature request.
That’s not to say that I think there’s a really good NPC capability in there now: I don’t. But I do think there is enough in there to show that we can do what we need.
The Making App
If this were a real game, we’d have cave diggers, avatar artists and interaction designers and story writers and dialog creators all beavering away to make the game larger and more rich. And those people would need tools, Domain Specific Languages, menu-driven entry systems, whatever we could invent to make their job easier.
I’ve tried to get interested in this idea, and since I am not those people, and I don’t want to do the job that those people would do in a real game, I can’t get much energy behind the idea. Still, maybe there’s something really enjoyable hiding in there.
There are two “Making App” areas that could actually be useful to me as a Codea Lua programmer. It could be useful to improve CodeaUnit, or to create new tools to help manage a larger Codea program.
Let’s think about the latter.
For Today …
What used to be here was a long screed as I explored the little app-in-progress called Making. Nothing of interest, and I know you don’t want to read my wondering what the heck it did.
One thing it does is produce a report from all the tabs of a project, all the classes created, and all the methods on the classes:
And it has a little thing that can read out all the current globals. I’ve used a version of that to check whether my app is creating globals, such as when you forget to say local
and create a global named x
or something.
That got me thinking about member variables.
What Might Be Helpful? …
I believe that D2’s objects are too connected. Too many of them know about too many others. As a related effect to that, there are times in the code where an object may or may not have a given piece of information, and the code takes advantage of the fact that an object is just a table and therefore you can just add another member variable by storing into it.
As the program grows, it becomes more and more valuable to have some things checked and/or constrained. It would be great to be able to list, for each class, not just its methods, but its other members, which will basically be member variables.
But Lua has no declarations for member variables. You just initialize them as you wish:
function GameRunner:init(testX, testY)
self.tileSize = 64
self.tileCountX = testX or 85 -- if these change, zoomed-out scale
self.tileCountY = testY or 64 -- may also need to be changed.
self:createNewDungeon()
self.cofloater = Floater(50,25,4)
self.musicPlayer = MonsterPlayer(self)
self.dungeonLevel = 0
self.requestNewLevel = false
self.playerRoom = 1
Bus:subscribe(self, self.createNewLevel, "createNewLevel")
Bus:subscribe(self, self.darkness, "darkness")
end
At the end of this init, we can be sure that a GameRunner instance has member variables tileSize
, ‘musicPlayer, and so on, all the ones listed there. But it could have others, since
createNewDungeon` could create member variables if it wanted to.
And in fact it does:
function GameRunner:createNewDungeon()
self.dungeon = Dungeon(self)
end
It even turns out that later, when we actually create a level, we create the monsters for that level, and we store that table in a GameRunner member variable named monsters
.
So there’s no telling what the member variables of a class might be: they can pop into and out of existence dynamically. That’s great when you want to do it, but not so great when you want to be sure you know what the heck is going on.
This is, of course, why languages with types and compile-time type checking are popular. Many, perhaps most developers prefer to give up the ease of creation that goes with a language like Lua or Smalltalk, for the safety that comes with a language like Java or C#.
We’re still thinking about our Making and a Making App. It would be “nice” if we could work out a way to pre-declare member variables, and maybe even fix it so that you can’t create them on the fly. It would make some things less convenient, but give us a bit more control and a bit more self-documentation in the code.
To do this thing, we’re going to need to understand some of the meta-stuff that makes up Codea.
First, let’s review what the Lua class
function does:
function class(base)
local c = {} -- a new class instance
if type(base) == 'table' then
-- our new class is a shallow copy of the base class!
for i,v in pairs(base) do
c[i] = v
end
c._base = base
end
-- the class will be the metatable for all its objects,
-- and they will look up their methods in it.
c.__index = c
-- expose a constructor which can be called by <classname>(<args>)
local mt = {}
mt.__call = function(class_tbl, ...)
local obj = {}
setmetatable(obj,c)
if class_tbl.init then
class_tbl.init(obj,...)
else
-- make sure that any stuff from the base class is initialized!
if base and base.init then
base.init(obj, ...)
end
end
return obj
end
c.is_a = function(self, klass)
local m = getmetatable(self)
while m do
if m == klass then return true end
m = m._base
end
return false
end
setmetatable(c, mt)
return c
end
This is the official version in Codea. And as you can see, it is rather meta and strange.
The fundamental deal is that we’re creating a table that “is” the class. It starts out empty, and if there is a base class listed in the call to class
, we shallow-copy it into our own table, c
. Since this is happening at compile time, and if the base class is defined before ours–and it had better be–our table will include entries for each method from the base class, name->function.
This table c
will be our new class, because we’re going to return it, and the assignment Foo=class() will store that table in the global Foo. c
is the class we’re creating.
Now, before we go on, let’s reflect that after the class table here is completed, we’ll go on to define methods, like this:
Foo = class()
function Foo:blerg(bah,mum)
...
end
We’ll need to explore where those definitions go, in particular what Foo is.
Moving right along in the class
function:
-- the class will be the metatable for all its objects,
-- and they will look up their methods in it.
c.__index = c
This bit of Lua arcana says that if anyone tries to access something in the table c, specifically a function, it has to be found there. At this moment, I’m with you–I’m not sure how this is going to work out.
Moving along … we are going to create a “metatable”:
-- expose a constructor which can be called by <classname>(<args>)
local mt = {}
mt.__call = function(class_tbl, ...)
local obj = {}
setmetatable(obj,c)
if class_tbl.init then
class_tbl.init(obj,...)
else
-- make sure that any stuff from the base class is initialized!
if base and base.init then
base.init(obj, ...)
end
end
return obj
end
This table implements the Lua arcanum __call
, which means that if you type Foo(arg1, arg1) against our new global Foo, that translates into a call to the provided function, with the object in question, in this case the global table Foo, as the first argument, and then whatever args are provided in the call, i.e. arg1, arg2.
The code, of course, is supposed to create an instance. It does it by creating a local variable, obj
, setting its metatable to c
, from the outer scope, and then, if the object implements init
, it calls init, passing the newly created object table obj
and the rest of the args. The init
fills in whatever member variables it wishes.
If the class does not implement init
but the base class does, then that will be called. If you have an init and your base class has one, you’re on your own to get everything sorted, as nearly as I can tell.
Then we return our new instance obj
.
What about that metatable? Right. Any table can have a metatable. In fact, any value at all can have a metatable, and most of them already do. The deal with a metatable is that when Lua goes to perform an operation on the object, the metatable gets involved. We don’t need to understand all the things that can happen but we need to understand at least one.
Our instance obj
is just an empty table, whose metatable is the one we created above, the class, which includes all the class’s methods, because when we say
function Foo:meth(a,b)
...
end
That’s exactly as if we had said
Foo.meth = function(a,b)
...
end
So in the table, here called c
but also soon to be found in global Foo
, there’ll be a function called meth
, but there will be none in our object.
So when, soon, someone says obj:meth(x,y)
, Lua will look in obj
and won’t find meth
, so it will look in the metatable, for __index
, which is the “thing to do if a table can’t find a key”, and it will find that __index=c
and will therefore look in c
, which is Foo
, and find the method we have put there.
All that mechanism just means “methods are not in the instance, they are in the class, and the instances have the class as their metatable”.
Now then …
The rest of the class function defines an is_a
method that will loop over the class and its superclasses, and return true if the object in hand is any one of them. We try not to use is_a
but sometimes it seems expedient.
Finally, we set the metatable of c
to the table mt
we’ve been working on, and return the class c
. I think we do it last like that to be sure that nothing we do during creation of c will invoke the metatable. That’s for instances, and we’re digging around in the meta-world here.
At this point, after we said Foo=class()
, the global Foo
contains a table that has a metatable. That metatable has just one member, the __call
to the function that makes instances and inits them. The instances get, as their metatable, the object c
, namely the class Foo
itself.
Weird, yes. I’ll see if I can draw a picture that will help.
But all this stuff means that you can call the global Foo
to create an instance of class Foo
and that that instance will look up its methods in the table Foo
. It will store its member variables in the instance itself.
We’re not there yet, but we’re getting closer.
Where the Heck are you Going??
We’re thinking about member variables and the fact that we don’t declare them. I wonder if we could implement a way to declare them in a batch, and then lock the instance against further creation of members. Then our batch creation could probably be documented somehow, in the same way that we read out the methods in the Making App.
There is a metatable entry like __index
, which we just looked at. The metatable entry __newindex
is used when you try to create a new key in a table, as when you write, with newKey
undefined:
self.newKey = 42
If the table has no entry __newindex
, the new key and value are created. But if the table has __newindex
, and it’s a function, the function will be called. The function could store the value somewhere else, or even go ahead and put it in.
It turns out that the above is wrong, though the documentation sure seems to say it. You can’t put
__newIndex
in the table itself: it has to go into the table’s metatable. We’ll get there, but I’m leaving this misapprehension in to show that we can in fact recover from mistakes like these … eventually.
There may be an issue, however, with nil
, because storing a nil actually removes the key from the table. Maybe we don’t care.
As a Spike, let’s create a new project called IXnewIndex. I pick the weird name so that Codea’s two-character icon will show IX and perhaps clue me in later.
I’m going to TDD a little something here.
_:test("Class members", function()
local f = Foo()
_:expect(f.x).is("x")
_:expect(f.y).is("y")
end)
This better fail looking for Foo.
1: Class members -- Tests:16: attempt to call a nil value (global 'Foo')
I love it when a plan comes together.
Permit me to do the whole class so far:
Foo = class()
function Foo:init()
self.x = "x"
self.y = "y"
end
Test runs. Now I want to have a method that creates new member variables, so that I can fix the class not to allow it.
_:test("Class members", function()
local f = Foo()
_:expect(f.x).is("x")
_:expect(f.y).is("y")
f:create("a")
_:expect(f.a).is("a")
end)
function Foo:create(varName)
self[varName] = varName
end
We can’t say self.varName
because that defines the variable named “varName”. So we subscript. Now I want to lock members, so I’ll call the imaginary function ‘lock’ and then demand that it should work:
_:test("Class members", function()
local f = Foo()
_:expect(f.x).is("x")
_:expect(f.y).is("y")
f:create("a")
_:expect(f.a).is("a")
f:lock()
_:expect(f:create("b")).throws("something")
_:expect(f.b).is(nil)
end)
I’m not sure how to implement that. Given an empty lock
, I get errors as you’d expect:
1: Class members -- Actual: nil, Expected: something
1: Class members -- Actual: b, Expected: nil
Now to make it throw something. I think we can set a function into __newindex
to make that happen.
I tried this:
function Foo:lock()
self.__newindex = function(obj,key,val)
error("tried to assign "..key)
return "tried to assign "..key
end
end
That seems to have no effect. I’m sure that the Lua writeup said it looks for __newindex
in the table. Let’s review that.
__newindex: The indexing assignment table[key] = value. Like the index event, this event happens when table is not a table or when key is not present in table. The metamethod is looked up in table.
Like with indexing, the metamethod for this event can be either a function or a table. If it is a function, it is called with table, key, and value as arguments. If it is a table, Lua does an indexing assignment to this table with the same key and value. (This assignment is regular, not raw, and therefore can trigger another metamethod.)
Whenever there is a __newindex metamethod, Lua does not perform the primitive assignment. (If necessary, the metamethod itself can call rawset to do the assignment.)
That sure seems to me to say that it looks in the table itself, not a metatable. I suspect it has to be in the metatable.
That means I can make this work but probably can’t get what I really need.
function Foo:lock()
local mt = getmetatable(self)
mt.__newindex = function(obj,key,val)
error("tried to assign "..key)
return "tried to assign "..key
end
end
Now the tests do this. Three OK, then
1: Class members -- Tests:43: tried to assign b
I think that use of throws
is wrong, I don’t do that often. Better find an example. Yes, it wants a function in the expect
:
_:test("Class members", function()
local f = Foo()
_:expect(f.x).is("x")
_:expect(f.y).is("y")
f:create("a")
_:expect(f.a).is("a")
f:lock()
_:expect(function() f:create("b") end).throws("something")
_:expect(f.b).is(nil)
end)
This should fail not matching “something”.
1: Class members -- Actual: function: 0x281556100, Expected: something
Well that’s odd. Here’s the example from CodeaUnit:
_:test("Thrown test", function()
_:expect(function()
error("Foo error")
end).throws("Foo error")
end)
Looks to me as if my __newindex
is returning the function, not calling it. Bother.
Ah, that’s not what happens. If the error that throws isn’t the exact one you expect, it returns the function rather than the wrong string. Might have to do something about that in CodeaUnit, but I think if I change the expect it should be OK.
_:test("Class members", function()
local f = Foo()
_:expect(f.x).is("x")
_:expect(f.y).is("y")
f:create("a")
_:expect(f.a).is("a")
f:lock()
_:expect(function() f:create("b") end).throws("tried to assign b")
_:expect(f.b).is(nil)
end)
This test runs. But have we now lost all our methods by virtue of changing the metatable? I fear so.
_:test("Class members", function()
local f = Foo()
_:expect(f.x).is("x")
_:expect(f.y).is("y")
f:create("a")
_:expect(f.a).is("a")
f:lock()
_:expect(function() f:create("b") end).throws("tried to assign b")
_:expect(f.b).is(nil)
_:expect(f:ok()).is("ok")
end)
Foo = class()
function Foo:init()
self.x = "x"
self.y = "y"
end
function Foo:create(varName)
self[varName] = varName
end
function Foo:ok()
return "ok"
end
function Foo:oops()
error("oops")
end
function Foo:lock()
local mt = getmetatable(self)
mt.__newindex = function(obj,key,val)
error("tried to assign "..key)
end
setmetatable(self, mt)
end
Finally …
Woot! The test runs! So methods are still found. Setting the metatable back into the instance seems OK. So far.
I need more testing, of multiple instances.
_:test("Multiple classes", function()
local f1 = Foo()
local f2 = Foo()
f1:create("a")
f2:create("a")
f1:lock()
f2:create("b")
_:expect(f2.b).is("b")
_:expect(function() f1:create("b") end).throws("tried to assign b")
_:expect(f1.b).is(nil)
end)
I had hoped that this would either work, or fail on the f2:create("b")
. But no, it fails out of the box:
2: Multiple classes -- Tests:64: tried to assign x
That has to be the very first creation. Which tells me that this test should fail similarly:
_:test("Class members", function()
local f = Foo()
_:expect(f.x).is("x")
_:expect(f.y).is("y")
f:create("a")
_:expect(f.a).is("a")
f:lock()
local canCreate = Foo()
_:expect(function() f:create("b") end).throws("tried to assign b")
_:expect(f.b).is(nil)
_:expect(f:ok()).is("ok")
end)
Yes. We have locked the entire metatable, which isn’t an incredible surprise. A little more experimenting tells me that the setmetatable
isn’t needed. Whatever reason I had for putting that in, it wasn’t necessary.
What about creating a new metatable in this instance? Can I make that work? I think maybe.
function Foo:lock()
local meta = getmetatable(self)
local mt = {}
mt.__newindex = function(obj,key,val)
error("tried to assign "..key)
end
mt.__index = meta
setmetatable(self, mt) -- turns out not to be needed
end
Here we get our existing metatable, and create a new one. We put our __newindex
into the new one, and set __index
in our new one to point to the existing one. Then we set our own metatable to our new one, which points to the old one for __index
, which will bounce twice and then find methods as before.
And both my tests run!
Let’s robustify a bit.
_:test("Multiple classes", function()
local f1 = Foo()
local f2 = Foo()
f1:create("a")
f2:create("a")
f1:lock()
f2:create("b")
_:expect(f2.b).is("b")
_:expect(function() f1:create("b") end).throws("tried to assign b")
_:expect(f1.b).is(nil)
local f3 = Foo()
f3:create("xxx")
_:expect(f3.xxx).is("xxx")
_:expect(function() f1:create("xxx") end).throws("tried to assign xxx")
end)
Test still runs! I think we’ve got it. We have a way to write a method lock
that will prevent further creation of member variables in that instance. It can still create new instances, and the methods are still looked up as they should be.
So the spike, I think, is a success. We have written 75 lines of code and test to determine that this method locks an instance from creating new member variables:
function Foo:lock()
local meta = getmetatable(self)
local mt = {}
mt.__newindex = function(obj,key,val)
error("tried to assign "..key)
end
mt.__index = meta
setmetatable(self, mt)
end
Here’s what we’ve done with lock()
:
Seventy-five lines to get nine. Not a great ROI, but this stuff is tricky. Now that we can lock an instance, we can look at ways to allow it to initialize members and then lock. That may be tricky, because nil members are removed not set to nil explicitly. I think we’ll be returning to this little project to deal with that.
Next time. For today, this will do just fine. I like going out on a win!
See you next time!