Menu
Coddy logo textTech

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
end

The 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 object

This 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 icon

Challenge

Easy

Let'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 print Error: Config is read-only instead 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 maxUsers field (which should trigger the error message)
    • Printing maxUsers again to prove it wasn't actually changed

You will receive three inputs:

  1. The application name (a string)
  2. The version (a string)
  3. 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: 100

Remember: 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
end

The proxy pattern uses an empty table (proxy) that the user interacts with, while the actual data lives in a separate internal table (data):

  • __index fetches values from the data table when reading
  • __newindex intercepts write attempts and blocks them
local p = ImmutablePoint.new(10, 20)
print(p.x)  -- 10 (reading works)
p.x = 50    -- Error: Cannot modify immutable object

This 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
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming