<sub>2025-04-04 @19:00</sub> #digital-garden #lua #programming #luajit # Read Only Table Reading through this Lua book, I see there is a read only pattern the author is using. ```lua function readOnly(t) local proxy = {} local mt = { __index = t, __newindex = function() error("read only") end } setmetatable(proxy, mt) return proxy end ``` I see why the author is explicitly writing everything out. It helps build a better understanding of proxy implementation. Some initial questions come to mind. - How would I print the table `t`? - Is it really read only? ## Printing the Table The table returned is just a regular Lua table. It is empty and has an associated metatable. Accesses on nonexistent keys redirect to lookups in `t`. Assignments to nonexistent keys trigger an error. We could create a function inside `readOnly` where we pass `self` (that is, `proxy`) to a function. Then we call `pairs` or `ipairs` on it. ```lua function readOnly(t) local proxy = {} function proxy:ipairs() return ipairs(t) end local mt = { __index = t, __newindex = function() error("read only") end } setmetatable(proxy, mt) return proxy end local days = readOnly({"mon", "tue"}) for k, v in days:ipairs() do print(k, v) end ``` I do not know. This does not seem quite right. It works and is great for practicing the use of colon notation. But I feel like the right approach here is to use the `__tostring` metamethod. That feels more in line with the current pattern. ```lua local function read_only(t) return setmetatable({}, { __index = t, __newindex = function() error("read only") end, __tostring = function() local output = {} for k, v in pairs(t) do table.insert( output, string.format("%s=%s", k, v) ) end return table.concat(output, ", ") end }) end ``` ## Read Only Is it really read only though? What is stopping me from unintentionally modifying or deleting the metatable? More importantly, what is stopping me from doing something like this? After all, I am just a human. ```lua days = read_only({"mon", "tue"}) getmetatable(days).__index[1] = "foo" ``` We can lock down the metatable using a metafield called `__metatable`. We set it to `false`. This would prevent calls to `getmetatable()` from modifying or even inspecting the metatable. ```lua local function read_only(t) return setmetatable({}, { __index = t, __newindex = function() error("read only") end, __tostring = function() local output = {} for k, v in pairs(t) do table.insert( output, string.format("%s=%s", k, v) ) end return table.concat(output, ", ") end, __metatable = false }) end ``` However, what if I load the debug library? If available, it may let me work around this. How far should I go in making something read only? At what point is enough really enough? ### To What Ends? There is some tension here when deciding whether or not `__metatable = false` is desirable. - Calling `getmetatable(days)` would return `nil` when I set `__metatable = false`. I should be aware that any attempts in my code to retrieve the metatable may silently fail or behave differently than expected. ```lua if getmetatable(days) then -- do something end ``` - Lua's strength is simplicity. Adding layers of protection, such as `__metatable = false`, may reduce that simplicity and clarity. Would another developer expect the metatable to be locked? Overprotecting the table may introduce unintended side effects. ### My Consensus Is I believe I am right to express tension in using `__metatable = false`. Not every implementation may need this. However, I do not think I am overstepping if the situation calls for it. For example, plugin development or exposed API design may warrant this extra care. My reasoning is as follows. - A function called `read_only` with `__metatable = false` clearly communicates intent. The intent is that this table should not have its behavior modified at runtime. It signals to myself and others that doing so is unsupported. It also communicates that such behavior is discouraged and could cause subtle bugs. - A common security phrase is "Defense in Depth". This suggests that good software practices include layering protections. Just because the debug library exists does not mean I should ignore the value of basic protection in normal operation. - I am not necessarily protecting against malicious attacks. I am also protecting against accidental misuse, confusion, or honest mistakes. This includes mistakes by others and by myself during development. - At any mature point in development, I can still lift these restrictions. Nothing is stopping me from removing `__metatable = false` later. Adding it now as a safeguard against myself seems perfectly reasonable.