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
endIn 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
EasyLet'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 onself. 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 publicnamefield 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
namefield (should printKeeper) - What happens when you try to access
keeper.secret(should benil) - What happens when you try to access
keeper._secret(should also benil)
- The keeper's
You will receive one input:
- 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: nilThe 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: nilCheat 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
endIn 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>")
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