This first edition was written for Lua 5.0. While still largely relevant for later versions, there are some differences.
The fourth edition targets Lua 5.3 and is available at Amazon and other bookstores.
By buying the book, you also help to support the Lua project.

17.3 – Revisiting Tables with Default Values

In Section 13.4.3, we discussed how to implement tables with non-nil default values. We saw one particular technique and commented that two other techniques need weak tables so we postponed them. Now it is time to revisit the subject. As we will see, those two techniques for default values are actually particular applications of the two general techniques that we have seen here: object attributes and memoizing.

In the first solution, we use a weak table to associate to each table its default value:

    local defaults = {}
    setmetatable(defaults, {__mode = "k"})
    local mt = {__index = function (t) return defaults[t] end}
    function setDefault (t, d)
      defaults[t] = d
      setmetatable(t, mt)
If defaults had not weak keys, it would anchor all tables with default values into permanent existence.

In the second solution, we use distinct metatables for distinct default values, but we reuse the same metatable whenever we repeat a default value. This is a typical use of memoizing:

    local metas = {}
    setmetatable(metas, {__mode = "v"})
    function setDefault (t, d)
      local mt = metas[d]
      if mt == nil then
        mt = {__index = function () return d end}
        metas[d] = mt     -- memoize
      setmetatable(t, mt)
We use weak values, in this case, to allow the collection of metatables that are not being used anymore.

Given these two implementations for default values, which is best? As usual, it depends. Both have similar complexity and similar performance. The first implementation needs a few words for each table with a default value (an entry in defaults). The second implementation needs a few dozen words for each distinct default value (a new table, a new closure, plus an entry in metas). So, if your application has thousands of tables with a few distinct default values, the second implementation is clearly superior. On the other hand, if few tables share common defaults, then you should use the first one.