Doing Tricks With Lua Method Notation
From WoWWiki
Lua has a trick for creating something that acts a little bit like Java classes, and a little less like C procedures. This can be used at various places to create simple, elegant bits of code that do precisely what you want.
Contents |
The Basics
What is Method Notation?
Lua uses the ':' (colon) in function calling syntax and in function definition syntax to include an extra, hidden parameter that makes this trick work. So
foo:blahblah(arg1, arg2)
is really syntactic sugar for passing foo as the first argument to foo.blahblah,
foo.blahblah(foo, arg1, arg2)
In the same way, we can use method notation to define a function:
function foo:blahblah(arg1, arg2) ... end
is shorthand for
foo.blahblah = function(self, arg1, arg2) ... end
where 'self' is the actual paramater name used inside the defined function.
Note that this is really just syntactic sugar -- you can define a function with regular notation (and include that extra first parameter) and call it with method notation, or you can define it with method notation and call it with regular notation (as long as you remember to pass an extra initial argument that makes sense). But _usually_ you'll define and call with the same notation, both regular or both method, simply because it's easier on your brain.
Why do I care?
Method notation can provide nifty means to parceling out calls to a variety of functions/methods by name; it can make code reuse easy; and it can cause less pollution of the global namespace. But enough flapping of lips ... on with the tricks.
Trick #1: OnEvent Handling
(I would like to credit here the originator of this trick, but I don' know who it is. If you know, please edit.)
MyAddon = {}
function MyAddon:OnEvent(event)
if (type(self[event]) == 'function') then
self[event](self, event);
else
self:Log("Unhandled event: ", event);
end
end
function MyAddon:PLAYER_ENTERING_WORLD(event)
...
end
function MyAddon:PLAYER_LEAVING_WORLD(event)
...
end
function MyAddon:OnLoad()
this:RegisterEvent('PLAYER_ENTERING_WORLD');
this:RegisterEvent('PLAYER_LEAVING_WORLD');
end
Let's see how this plays out. The xml script contains
<OnEvent>MyAddon:OnEvent(event)</OnEvent>
so when a PLAYER_ENTERING_WORLD event is generated, it looks in MyAddon for member 'OnEvent', and calls it as a function, with arguments MyAddon and event...
MyAddon.OnEvent(MyAddon, event);
When MyAddon.OnEvent receives the call, it has the argument, self, which it then tries to index self with 'PLAYER_ENTERING_WORLD', it finds a function, and calls it passing self as the first arg (i.e. using regular notation but method style). So control passes to MyAddon.PLAYER_ENTERING_WORLD.
This may seem rather messy if you're only catching two or three events, but if you're catching 5 or 10, it avoids the huge sawtooth if-then-elseif mess if checking all the events.
Tricks
Trick #2: Code Reuse
There are lots of ways to get code reuse, but here's one way to do it with method notation. In a lot of my addons I log certain kinds of events to the chat window, so I like to have nice logging facilities that are easy to use. Logging is also great during debugging. It's easy enough to cut-and-paste the logging tools into every addon, but here's another way. Log and Msg both put out messages to a chat window, but Log includes a standard prefix to identify the program.
Utils = {logPrefix='Utils2.0: ', logR=0.5, logG=0.5, logB=0.5};
function Utils:Log(...)
self:Msg(self.logPrefix, unpack(arg));
end
function Utils:Msg(...)
local line = '';
for i,eachArg in ipairs(arg) do
line = line .. tostring(eachArg);
end
local chatFrame = DEFAULT_CHAT_FRAME or error('No chat frame found');
chatFrame:AddMessage(line, self.logR, self.logG, self.logB);
end
function Utils:EnableLogging(class, prefix, r, g, b) class.logPrefix = prefix or ''; class.logR = r or 0.5; class.logG = g or 0.5; class.logB = b or 0.5; class.Msg = self.Msg; class.Log = self.Log; end
function Utils:OnLoad()
self:Log('Loaded.');
end
Assuming a chat frame was there when the addon loaded, there would be a gray load message put into the chat frame. Note that EnableLogging isn't being called here.
But if we have two or three other addons that want logging, then...
MyFirstAddon = {};
...
function MyFirstAddon:OnLoad()
Utils:EnableLogging(self, 'Hyper Addon 0.8: ', 1, 0, 0);
self:Log('Loaded.');
end
when this addon loads there will be a bright red chat line announcing 'Hyper Addon 0.8: Loaded.'
What's happening here? There is only one copy of Log and Msg in the address space, but both Utils and MyFirstAddon have a pointer to it. Because we're using method notation, the first argument to each is the 'class', and the class has different logPrefix and logR/logG/logB values, so the same code can be used for logging multiple classes.
(Still under construction.)
