<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.