Read-Only Tables
Part of the Object Oriented Programming section of Coddy's Lua journey — lesson 46 of 70.
Closures provide true privacy, but they require defining methods inside the constructor. There's another approach for a different use case: making an object read-only after creation. This is useful when you want fields to be visible but unchangeable—like coordinates of a fixed point.
The __newindex metamethod intercepts attempts to assign new values to a table. By making it block all assignments, you can freeze an object:
local ImmutablePoint = {}
function ImmutablePoint.new(x, y)
local data = {x = x, y = y}
local proxy = {}
setmetatable(proxy, {
__index = data,
__newindex = function()
error("Cannot modify immutable object")
end
})
return proxy
endThe trick is using a proxy table. The actual data lives in data, while the user interacts with the empty proxy.
When reading, __index fetches from data. When writing, __newindex triggers instead—and we block it:
local p = ImmutablePoint.new(10, 20)
print(p.x) -- 10 (reading works)
p.x = 50 -- Error: Cannot modify immutable objectThis pattern is valuable for configuration objects, mathematical constants, or any data that should never change after initialization. The object remains fully readable—you just can't alter it.
Challenge
EasyLet's build an ImmutableConfig class that creates read-only configuration objects! Once created, these config objects should allow reading their settings but completely block any attempts to modify them.
You'll organize your code across two files:
ImmutableConfig.lua: Create a module that produces frozen configuration objects. Your.new(appName, version, maxUsers)function should store these three values internally, then return a proxy table that allows reading all fields but prevents any modifications. When someone tries to change a value, it should printError: Config is read-onlyinstead of raising an actual error.main.lua: Bring your ImmutableConfig to life! Read three inputs for the configuration values, create a config object, then demonstrate that it works correctly by:- Printing all three original values
- Attempting to modify the
maxUsersfield (which should trigger the error message) - Printing
maxUsersagain to prove it wasn't actually changed
You will receive three inputs:
- The application name (a string)
- The version (a string)
- The maximum users (a number)
Your output should show the config values, the error when modification is attempted, and confirmation that the value remained unchanged:
App: {appName}
Version: {version}
Max Users: {maxUsers}
Error: Config is read-only
Max Users: {maxUsers}For example, if the inputs are MyApp, 2.0, and 100, the output should be:
App: MyApp
Version: 2.0
Max Users: 100
Error: Config is read-only
Max Users: 100Remember: the proxy pattern uses an empty table that the user interacts with, while the actual data lives in a separate internal table. The __index metamethod fetches from the data table, and __newindex intercepts (and blocks) any write attempts!
Cheat sheet
The __newindex metamethod intercepts attempts to assign new values to a table. By blocking all assignments, you can create immutable (read-only) objects:
local ImmutablePoint = {}
function ImmutablePoint.new(x, y)
local data = {x = x, y = y}
local proxy = {}
setmetatable(proxy, {
__index = data,
__newindex = function()
error("Cannot modify immutable object")
end
})
return proxy
endThe proxy pattern uses an empty table (proxy) that the user interacts with, while the actual data lives in a separate internal table (data):
__indexfetches values from thedatatable when reading__newindexintercepts write attempts and blocks them
local p = ImmutablePoint.new(10, 20)
print(p.x) -- 10 (reading works)
p.x = 50 -- Error: Cannot modify immutable objectThis pattern is useful for configuration objects, mathematical constants, or any data that should remain unchanged after initialization.
Try it yourself
-- Import the ImmutableConfig module
local ImmutableConfig = require('ImmutableConfig')
-- Read inputs
local appName = io.read()
local version = io.read()
local maxUsers = tonumber(io.read())
-- TODO: Create a config object using ImmutableConfig.new()
-- TODO: Print all three original values in the format:
-- App: {appName}
-- Version: {version}
-- Max Users: {maxUsers}
-- TODO: Attempt to modify the maxUsers field (this should trigger error message)
-- TODO: Print maxUsers again to prove it wasn't changed
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Object Oriented Programming
1The 'Self' Concept
Tables with FunctionsExplicit 'self'The Colon SyntaxDot vs ColonRecap - Moving Point4Project: Digital Bank
Project SetupDeposit Method7Polymorphism & Overriding
Overriding MethodsCalling Parent MethodsDuck TypingCommon InterfaceChecking TypeRecap - Employee Roles10Project: Shape Manager
Project SetupRectangle Class2Class Prototype Pattern
The Prototype ConceptLinking with __indexThe :new() ConstructorInitializing AttributesIndependent InstancesRecap - Car Factory5Operator Overloading in OOP
Adding ObjectsSubtracting ObjectsConcatenating ObjectsComparing Objects (<, >)Recap - Wallet Math8Encapsulation
Naming ConventionsClosures for PrivacyAccess via ClosuresRead-Only TablesValidation LogicRecap - Secure Vault11Design Patterns (Lite)
Factory FunctionsSingleton TableIterator PatternObserver (Listener)Recap - Logger Factory3Object State and Behavior
Instance VariablesGetter MethodsSetter MethodsCalculated PropertiesFormatting StringsEquality ChecksRecap - Student Grade6Inheritance Basics
The Inheritance SetupInheriting MethodsExtending the ConstructorAdding Child MethodsShared vs UniqueRecap - Shape Hierarchy