Menu
Coddy logo textTech

Closures for Privacy

Part of the Object Oriented Programming section of Coddy's Lua journey — lesson 44 of 70.

Naming conventions signal intent, but they don't actually prevent access. Anyone can still write player._health = 999 and it works. For true privacy, Lua offers a different approach: storing data in local variables inside the constructor instead of on self.

The key insight is that local variables inside a function are only accessible within that function—and any functions defined inside it. This is called a closure:

local Counter = {}
Counter.__index = Counter

function Counter:new()
    local count = 0  -- Truly private! Not on self.
    
    local obj = {}
    setmetatable(obj, Counter)
    return obj
end

In this example, count exists only inside the :new() function. It's not stored in obj, so there's no way to access it from outside—myCounter.count returns nil, and there's no underscore-prefixed field to bypass.

The variable count lives in the closure's scope. It persists as long as the object exists, but it's completely hidden from external code. Unlike self._count, which is just a polite suggestion, this approach makes the data genuinely inaccessible.

Of course, hidden data isn't useful if you can never interact with it. In the next lesson, you'll learn how to define methods inside the constructor that can read and modify these private variables—giving you controlled access while maintaining true encapsulation.

challenge icon

Challenge

Easy

Let's build a SecretKeeper class that demonstrates true data privacy using closures! Unlike the underscore convention from the previous lesson, this approach makes data genuinely inaccessible from outside the object.

You'll organize your code across two files:

  • SecretKeeper.lua: Create a class where the secret is stored in a local variable inside the constructor—not on self. The constructor :new(secretValue) should store the secret in a local variable that exists only within the closure. For now, the object won't have any way to access this secret (that comes in the next lesson!), but you should also store a public name field set to "Keeper" on the instance so we can verify the object works.
  • main.lua: Require your SecretKeeper module and read a secret value from input. Create a SecretKeeper instance with that secret. Then demonstrate that the secret is truly private by printing three things:
    • The keeper's name field (should print Keeper)
    • What happens when you try to access keeper.secret (should be nil)
    • What happens when you try to access keeper._secret (should also be nil)

You will receive one input:

  1. The secret value to store (e.g., TopSecret123)

Your output should be three lines showing that while the public field works, the secret is completely hidden:

Name: Keeper
secret: nil
_secret: nil

The output will be the same regardless of what secret you pass in—because there's no way to access it! The local variable inside the constructor is invisible to the outside world. This proves that closure-based privacy is fundamentally different from the underscore naming convention.

For example, if the input is MyPassword, the output should still be:

Name: Keeper
secret: nil
_secret: nil

Cheat sheet

To create truly private data in Lua, store it in local variables inside the constructor instead of on self. This uses a closure—local variables are only accessible within the function where they're defined and any functions defined inside it.

Example of a class with private data:

local Counter = {}
Counter.__index = Counter

function Counter:new()
    local count = 0  -- Truly private! Not on self.
    
    local obj = {}
    setmetatable(obj, Counter)
    return obj
end

In this example, count exists only inside the :new() function. It's not stored in obj, so myCounter.count returns nil. There's no way to access it from outside—unlike self._count (which is just a naming convention), this approach makes the data genuinely inaccessible.

The variable persists as long as the object exists but remains completely hidden from external code, providing true encapsulation.

Try it yourself

-- Require the SecretKeeper module
local SecretKeeper = require('SecretKeeper')

-- Read the secret value from input
local secretValue = io.read()

-- TODO: Create a SecretKeeper instance with the secret value

-- TODO: Print the keeper's name field (format: "Name: <name>")

-- TODO: Print what happens when accessing keeper.secret (format: "secret: <value>")

-- TODO: Print what happens when accessing keeper._secret (format: "_secret: <value>")
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