Coding Tips

From WowAce Wiki
Jump to: navigation, search

New tips:

 ==Short Description==
 A longer description would go here.
 
 ===Example===
 Don't:
  bad example here
 Do:
  good example here
 
 ===Discussion===
 * I feel ____ about this tip.
 : --~~~~

Avoid using globals

Globals can cause issues because they may overwrite other people's functions, as well as being general bad practice. By not using globals and instead using a namespace format, one can gain quite a bit anyway, such as nice data storage, inheritance capabilities, and the usage of metatables. Note: Ace's standard method is to use objects as namespaces.

For most addons, they should only have 2 globals: The addon object and its saved variable table.

Example

Don't:

MyAddon_value = 0
function MyAddon_DoSomething()
    MyAddon_value = MyAddon_value + 1
end

Do:

MyAddon = {}
MyAddon.value = 0
function MyAddon:DoSomething()
    self.value = self.value + 1
end

If you do not plan on having the variable to be used outside of your addon you can make the variable local:

local value = 0
MyAddon = {}
function MyAddon:DoSomething()
    value = value + 1
end

Discussion

  • Avoiding globals is indeed a very good advice. You should avoid as much as possible using globals, even when reading. Accessing a local variable is O(1) in lua, accessing a global variable is a table lookup, so O(log n).
-- Jerry 03:44, 1 January 2007 (EST)
  • You can use "local g=getfenv()" to speed up global lookups.
--Industrial 06:39, 1 January 2007 (EST)
  • Using local _G = getfenv(), then accessing _G.SomeValue is much faster than using getglobal("SoleValue") (for repeated calls), but it is actually slightly slower than just accessing SomeValue as a normal global.
--Ckknight 06:55, 1 January 2007 (EST)
  • "G.SomeGlobal" is actually ~10-15% faster than just "SomeGlobal" (for repeated calls). Presumably because "SomeGlobal" ends up doing the equivalent of a getfenv() each time internally (in C, so much faster than doing it from Lua, obviously).
--Mikk 00:19, 16 July 2007 (EDT)


local a = _G.SomeValue
	3	[3]	GETTABLE 	1 0 -2	; "SomeValue"


local b =  getglobal("SoleValue")
	4	[5]	GETGLOBAL	2 -3	; getglobal
	5	[5]	LOADK    	3 -4	; "SoleValue"
	6	[5]	CALL     	2 2 2


local c = SomeValue
	7	[7]	GETGLOBAL	3 -2	; SomeValue

Function syntactical sugar

There are a few nice syntactical sugary bits that lua provides for function declarations and calls.

Normal functions

function Alpha() end

is the same as

Alpha = function() end

Methods

function Object:Bravo() end

is the same as

function Object.Bravo(self) end

is the same as

Object.Bravo = function(self) end


And for calling,

Object:Bravo()

is the same as

Object.Bravo(Object)
 This shows the slight difference between the two

 Object:Bravo()
	14	[17]	GETGLOBAL	0 -2	; Object
	15	[17]	SELF     	0 0 -3	; "Bravo"
	16	[17]	CALL     	0 2 1


 Object.Bravo(Object)
	17	[19]	GETGLOBAL	0 -2	; Object
	18	[19]	GETTABLE 	0 0 -3	; "Bravo"
	19	[19]	GETGLOBAL	1 -2	; Object
	20	[19]	CALL     	0 2 1

Colon calling and inheritance

Since lua passes the self as the first argument with the colon syntax, if you have an __index in your metatable, it can act very nicely for you. Although this applies to user-defined tables, strings also work with the colon syntax.

("foo bar"):sub(3, 5) == "o b"

is the same as

("foo bar").sub("foo bar", 3, 5) == "o b"

is the same as

string.sub("foo bar", 3, 5) == "o b"

Since every string inherits from the string table.


 This shows how each is interpreted slightly differenltly. Only the differences are shown.

 local a = ("foo bar"):sub(3, 5) == "o b"
	1	[1]	LOADK    	0 -1	; "foo bar"
	2	[1]	SELF     	0 0 -2	; "sub"


 local b = ("foo bar").sub("foo bar", 3, 5) == "o b"
	10	[3]	LOADK    	1 -1	; "foo bar"
	11	[3]	GETTABLE 	1 1 -2	; "sub"
	12	[3]	LOADK    	2 -1	; "foo bar"


 local c = string.sub("foo bar", 3, 5) == "o b"
	20	[5]	GETGLOBAL	2 -6	; string
	21	[5]	GETTABLE 	2 2 -2	; "sub"
	22	[5]	LOADK    	3 -1	; "foo bar"

Discussion

Lua has quite a few nice sugary bits.

Ckknight 10:17, 31 December 2006 (EST)

Sugar rots your teeth, and generally has the same effect on the readability of your code.

--Feasmira 12:59, 14 February 2007 (EST)

Avoid over-use of tables

Blank tables cost 36 bytes per. Though many times tables are the right way to go, try not to use them wildly.

Example

Don't:

local color = { 1, 0.5, 0 }

Do:

local r, g, b = 1, 0.5, 0

Discussion

Recycle tables

Lua 5.1 introduced incremental garbage collection, so collection cycles no longer mean long pauses, and it's fairly efficient.

However, for small tables, needed "all the time" (basically "without user interaction") it is still more efficient to reycle them rather than letting the GC handle them.


Example

Don't:

local function MyFunction()
    local tmp = {} -- defining the function here means it will be created
                   -- and GCed every time the function is called
    -- do things to the table
end

Do:

local tmp = {}
local function MyFunction()
    -- do things to the table
    
    for k in pairs(tmp) do
        tmp[k] = nil -- this section clears the temporary table
    end
end

Alternatively, if you need multiple tables or recursive calling:

local cache = setmetatable({}, {__mode='k'}) -- this metatable makes the keys weak,
                                             -- and able to be garbage-collected
local function MyFunction()
    local tmp = next(cache) or {} -- take from cache or new
    cache[tmp] = nil  -- remove from cache
    
    -- do things to the table
   
    for k in pairs(tmp) do
        tmp[k] = nil -- this section clears the temporary table
    end
    cache[tmp] = true -- add back to cache
end

Discussion

  • Note that for hashes (tables that don't just have indices 1--n), the internals of the table object never really shrink back. See [Lua object memory sizes on wowwiki, and its associated talk page for the discussion on efficiency.
Basically, generic "table composts" can cause more harm than they do good. Local, specialized table recycling can be good, given that your table fits the pattern (it's a hash, it's small).
--Mikk 12:21, 19 July 2007 (EDT)

Metatables for dynamic programming/caching

There are cases when you want to call a function that returns a value, but if you were to call it again it would call the same value. In intensive functions, and especially in recursive functions, much of the time spent on computation can be fixed by using a cache.

Example

For this example, we're going to use the fibonacci sequence, which is defined as
fib(0) = 0
fib(1) = 1
fib(n) = fib(n-2) + fib(n-1)

Without caching:

function fib(n)
    if n <= 1 then
        return n
    else
        return fib(n-2) + fib(n-1)
    end
end
print(fib(30)) -- 832040

If you were to try to solve fib(30) without caching, it would take 2692537 calls to the fib function. If you want to use the cache which we will show you next, it will only take 28 calls.

With caching:

fib = setmetatable({[0] = 0, [1] = 1}, { __index = function(self, n)
    local value = self[n-2] + self[n-1] -- gets the previous values, possibly from the cache, possibly from calculation.
    self[n] = value -- this causes the value to cache into the table
    return value -- remember to return the proper result
end })
print(fib[30]) -- 832040

A simpler example

squares = setmetatable({}, { __index = function(self, n)
    local value = n^2 -- calculate
    self[n] = value -- store in cache
    return value -- return result
end })

assert(squares[3] == 9) -- calculates this time
assert(squares[3] == 9) -- uses cache
assert(squares[3] == 9) -- uses cache

This is a nice example in that it shows a simple, nonrecursive cache that can be very handy when used for more complex structures.

Discussion

The fibonacci example is more a comparison between recursive and iterative programming, but shows nicely how metatables can be used for programming. --Trh

Nice way to register events (without a framework)

In the OnEvent script, you can use the event as a key to a table, and assuming that it's a function, run that.

Example

MyAddon = {}
function MyAddon:SOME_EVENT(arg1, arg2)
    -- code
end
function MyAddon:SOME_OTHER_EVENT(arg1, arg2)
    -- code
end
local frame = CreateFrame("Frame")
frame:RegisterEvent("SOME_EVENT")
frame:RegisterEvent("SOME_OTHER_EVENT")
frame:SetScript("OnEvent", function(this, event, ...)
    MyAddon[event](MyAddon, ...)
end)

Discussion

This is basically how AceEvent-2.0 handles it, only a little more complex.

Ckknight 23:29, 3 January 2007 (EST)

Is this way to register events faster than the "traditional one"?

Jaliborc 13:35, 10 November 2007 (GMT)

Registering events with AceEvent-2.0 sanely

AceEvent-2.0 is available to make your event handling easier, and it is different than Blizzard's event handling, as much of it is done for you already.

A common mistake is to have one giant event handler, which AceEvent-2.0 has rendered unnecessary.

Example

Don't:

function MyAddon:OnEnable()
    MyAddon:RegisterEvent("PLAYER_REGEN_ENABLED", "OnEvent")
    MyAddon:RegisterEvent("PLAYER_REGEN_DISABLED", "OnEvent")
end

function MyAddon:OnEvent()
    if event == "PLAYER_REGEN_ENABLED" then
        -- code
    elseif event == "PLAYER_REGEN_DISABLED" then
        -- code
    end
end

Do:

function MyAddon:OnEnable()
    MyAddon:RegisterEvent("PLAYER_REGEN_ENABLED")
    MyAddon:RegisterEvent("PLAYER_REGEN_DISABLED")
end

function MyAddon:PLAYER_REGEN_ENABLED()
    -- code
end

function MyAddon:PLAYER_REGEN_DISABLED()
    -- code
end

This is also much faster and more efficient.

Discussion

  • Why is AceEvent-2.0 "much faster"? Except the disadvantage of requiring the if else chain in OnEvent handler, what else makes the Blizzard event handling slow? what exactly makes it unefficient?
--Rophy 01:28, 1 January 2007 (EST)
  • One advantage is that you don't need to create a frame to listen to event, as the AceEvent frame will be used. When you think about dozen addons running at the same time, having one table lookup done by AceEvent is also way faster than many little if-else-chain by each addon.
--Jerry 03:39, 1 January 2007 (EST)
  • It wasn't being disparaging to raw handling of events, it was to inform that you shouldn't treat AceEvent-2.0's event handling the same way you would Blizzard's.
--Ckknight 06:31, 1 January 2007 (EST)
  • I think the question still stands: "why is AceEvent-2.0 'much faster'?" Do you mean much faster to code?
--Fin 19:55, 9 June 2007 (CET+1)
  • Because a table lookup (to find the handler) is faster than a long if/elseif/elseif chain. Of course, you can create such a table + lookup yourself and get the same speed.
--Mikk 01:05, 16 July 2007 (EDT)

Do not put everything on one line

One-liners are very bad practice from a usability standpoint, especially if you're trying to grasp what an algorithm is doing at a glance.

Example

Don't:

if condition then DoALotOfCodeRightHere(); AndMoreRightHere(); AndMoreHere() end

Do:

if condition then
    DoALotOfCodeRightHere()
    AndMoreRightHere()
    AndMoreHere()
end

Even though you can boast in the first one "Look what I did in only K lines of code!", it's going to be a bitch to take care of later, or if someone else needs to fix it. Use the second version, even for simple tasks.

This goes for if statements, do statements, while loops, for loops, repeat-until loops, functions, etc. Spreading your code out is a good thing.

Discussion

I hate when people boast about their 10-line addon that if spread out properly is actually 10x the size or so.

Ckknight 23:47, 31 December 2006 (EST)

I boast about my 4kb addons.

Industrial 06:45, 1 January 2007 (EST)

Lua and strings

In lua, strings are interned. This means that there is only one copy of each string in lua's memory and that simple hash values are kept as values. This fact has a lot of implications, and you should be really aware of this fact.

Example

This means that

foo == "v"

and

foo == "SomeReallyLongStringValue"

have the same complexity (constant time).

This also means that something like the following should really be avoided :

local s = ""
while condition do
  s = s .. "some string"
end

Remember that for a simple concatenation, lua will need to internalize the string value, which will use memory that's going to be garbage collected. Sometimes, even doing the following might be faster and cheaper, even if you construct a table that's also using memory and also will need to be garbage collected :

local s = {}
while condition do
 table.insert(s, "some string")
end
s = table.concat(s, "")

Discussion

The optimisation of lua code is sometimes surprising, it's crucial to understand the way lua works. Jerry 04:31, 1 January 2007 (EST)

The table.concat operation and the .. operation are the same. If you unrolled the loop in the example you would achieve the same result.Sylvanaar 04:31, 17 September 2007 (EST)

Yes, string1..string2 is essentially equivalent to table.concat{string1, string2}, but that's not what's going on in this example. Every iteration of the loop creates an entirely new string. What this does is more like table.concat{table.concat{table.concat{table.concat{string1, string2}, string3}, string4}, string5} and on as many times as the loop runs.
What you should really use is a recursive vararg function that returns multiple values and then use WoW's strconcat function to concatenate them. Cogwheel 23:14, 26 September 2007 (EDT)

Cost of table lookup

Due to the way metatables works and the fact that the lua compiler is really simple, table lookup is always done every time it appears in the code.

Example

Don't:

local foo = {}
foo.bar = {}
foo.bar.x = 1
foo.bar.y = 2

Do:

local foo = {
    bar = { 
        x = 1,
        y = 2
    }
}

Also,

Don't:

function MyAddon:Initialize()
    if self.db.profile.foo then
        self.db.profile.bar = self.db.profile.something + 1
        self.db.profile.bar2 = self.db.profile.something + 2
    end
end

Do:

function MyAddon:Initialize()
    local p = self.db.profile
    if p.foo then
        local something = p.something
        p.bar = something + 1
        p.bar2 = something + 2
    end
end

Be aware that in the previous example, if 'self', 'self.db' or 'self.db.profile' have weird metatables, then the two function might not behave exactly the same way.

Discussion

Table lookup has a cost. It's small, but it's there. It's easy to avoid it most of the time, and it may make your code easier to read. Remember that accessing a global variable is a table lookup too.

Jerry 04:44, 1 January 2007 (EST)



Traversing over a table with non-numerical keys in order

Given a table, for example,

local myTable = {
    India = "juliet",
    alpha = "bravo",
    echo = "foxtrot"
    Charlie = "delta",
    golf = "hotel",
}

Unordered traversal:

for k,v in pairs(myTable) do
    -- code here
end
-- or --
for k, v in next, myTable do
    -- code here
end

Ordered traversal:

local tmp = {}
local function ignoreCaseSort(alpha, bravo)
    return alpha:lower() < bravo:lower()
end
for k in pairs(myTable) do
    tmp[#tmp+1] = k
end
table.sort(tmp, ignoreCaseSort)
for i,k in ipairs(tmp) do
    tmp[i] = nil -- clears the temporary table for next time you want to use it
    local v = myTable[k]
    
    -- code here
end

The two traversals have the exact same effect, except that the second traversal does it in the order you specify.

Discussion

Make a plan before you start

Before you write a single line of code, even before you create the .toc file for your addon, you should spend some time (30+ minutes) planning exactly what the code structure and user-interface elements of your addon will look like.

This means:

  • Draw your user-interface on a sheet of paper (or do a mockup in gimp/photoshop/paint/etc), so you know where you're headed.
  • Write out all data-fields of all tables that you're going to use; include the data-type of each field, and a brief description of what is stored there.
  • Write out the headers for functions that you know that you're going to need. Including:
    • A brief description of what the function does;
    • A description of what its input variables are;
    • A description of any return values for the function;
    • Mentioning any global variables you expect the function will need to access and/or modify.

Ideally, you would include this documentation in some sort of developer readme with your addon. It will help you when maintaining the addon down the road, and it'll help any other developer who wants to help you out with some coding.

Discussion

Feel free to do your documentation in wiki-form. If you're writing an Ace or Ace-related addon, it is recommended to document on the wowace wiki, otherwise you can use a wiki of your choice.

Ckknight 23:13, 2 January 2007 (EST)

Easy multi-dimensional tables

With metatables, you can very easily create 2-dimensional, k-dimensional, or *-dimensional tables.

Example

Two-dimensional:

local matrix = setmetatable({}, {__index = function(self, id)
    local t = {}
    self[id] = t
    return t
end})
matrix[1][1] = "Agent Smith"

Even though this is three-dimensional, it could be easily modified to supply any finite number of dimensions.

local mt = {__index = function(self, id)
    local t = {}
    self[id] = t
    return t
end})
local threeD = setmetatable({}, {__index = function(self, id)
    local t = setmetatable({}, mt)
    self[id] = t
    return t
end)}
threeD.alpha.bravo.charlie = "delta"

*-dimensional, or infinite-dimensional

local mt; mt = {__index = function(self, id)
    local t = setmetatable({}, mt)
    self[id] = t
    return t
end})
local starD = setmetatable({}, mt)
starD.this.is.making.quite.a.few.tables.and.is.not.necessarily.an.efficient.solution = true

Discussion

In many cases, you should typically use a flat table, for efficiency's sake, but this works if you need multi-dimensional arrays.

Ckknight 21:50, 4 January 2007 (EST)

Don't put localized strings into SVs

Since EU clients can switch between locales, try to not put localized strings into the Saved Variables. It is recommended to either use English, not use a specific language, or to use IDs instead of fully localized strings.

Discussion