<!-- category: snippets -->
Tied Scalars: Variables That Spy on Themselves
What if every time you read a variable, it computed its value on the fly? What if every time you wrote to a variable, it validated the input first? What if a plain$x = 5 could
trigger logging, enforce types, or reject bad data?
That looks like a normal scalar. It acts like a normal scalar. But behind the scenes, custom code fires on every read and every write. The variable is spying on itself.tie my $temp, 'Celsius'; $temp = 100; say $temp; # 100 say "Boiling!" if $temp >= 100; $temp = -300; # dies: "Temperature below absolute zero"
This is tie. Perl's mechanism for making ordinary-looking variables do extraordinary things.
Part 1: WHAT TIE DOES
Thetie function binds a variable to a class. After that,
every operation on the variable (read, write, destroy) calls a
method on that class instead of doing the normal thing.
From the outside,tie my $var, 'MyClass'; $var = 42; # calls MyClass->STORE($value) my $x = $var; # calls MyClass->FETCH() untie $var; # calls MyClass->UNTIE() then DESTROY()
$var is just a scalar. You use = to assign,
you interpolate it in strings, you pass it to functions. Nothing
looks different. But every access goes through your code.
It is like putting a transparent proxy in front of a variable. The user never knows the proxy is there.
Part 2: THE INTERFACE
A tied scalar class needs exactly three methods. That is the entire contract:package MyTiedScalar; sub TIESCALAR { my ($class, @args) = @_; # called by tie() # return the object that represents this tied variable return bless { value => undef }, $class; } sub FETCH { my ($self) = @_; # called when the variable is read return $self->{value}; } sub STORE { my ($self, $value) = @_; # called when the variable is written $self->{value} = $value; } 1;
TIESCALAR is the constructor. It runs when you call tie. The
object it returns is the hidden state behind the variable.
FETCH runs on every read. Return whatever the variable should
appear to contain.
STORE runs on every write. Do whatever you want with the
incoming value.
That is it. Three methods and you have a custom variable.
Part 3: A MINIMAL EXAMPLE
A variable that counts how many times it has been read:Thepackage SpyScalar; sub TIESCALAR { my ($class) = @_; return bless { value => undef, reads => 0 }, $class; } sub FETCH { my ($self) = @_; $self->{reads}++; return $self->{value}; } sub STORE { my ($self, $value) = @_; $self->{value} = $value; } sub read_count { my ($self) = @_; return $self->{reads}; } package main; use feature 'say'; tie my $secret, 'SpyScalar'; $secret = "classified"; say $secret; # read 1 say $secret; # read 2 my $copy = $secret; # read 3 my $obj = tied $secret; # get the underlying object say "Read ${\$obj->read_count} times"; # 3
tied function returns the object behind a tied variable.
Through it you can call any methods on the class, not just the
tie interface methods.
Part 4: VALIDATION ON WRITE
Reject bad data the moment it touches the variable:The variable enforces its own contract. You cannot put garbage in it. The validation is baked into the variable itself, not scattered across every piece of code that touches it.package PositiveInt; sub TIESCALAR { my ($class) = @_; return bless { value => 0 }, $class; } sub FETCH { my ($self) = @_; return $self->{value}; } sub STORE { my ($self, $value) = @_; die "Not a number: $value\n" unless $value =~ m~^-?\d+$~; die "Must be positive: $value\n" unless $value > 0; $self->{value} = $value; } package main; use feature 'say'; tie my $count, 'PositiveInt'; $count = 42; # fine say $count; # 42 $count = -5; # dies: "Must be positive: -5" $count = "banana"; # dies: "Not a number: banana"
Part 5: LOGGING EVERY ACCESS
Watch everything that happens to a variable. Useful for debugging mysterious mutations:Every read and write is logged with the variable name and value. Tie it during debugging, untie it when you are done. The rest of your code never changes.package LoggedScalar; use feature 'say'; sub TIESCALAR { my ($class, $name) = @_; return bless { value => undef, name => $name }, $class; } sub FETCH { my ($self) = @_; say STDERR " [READ] \$$self->{name} => " . ($self->{value} // 'undef'); return $self->{value}; } sub STORE { my ($self, $value) = @_; say STDERR " [WRITE] \$$self->{name} = " . ($value // 'undef'); $self->{value} = $value; } package main; tie my $x, 'LoggedScalar', 'x'; $x = 10; # [WRITE] $x = 10 $x = $x + 5; # [READ] $x => 10 # [WRITE] $x = 15 $x++; # [READ] $x => 15 # [WRITE] $x = 16
Part 6: COMPUTED VALUES
A variable that computes its value on the fly. It never stores anything. Every read is fresh:The variablepackage Timestamp; use POSIX qw(strftime); sub TIESCALAR { my ($class, $format) = @_; return bless { format => $format // '%Y-%m-%d %H:%M:%S' }, $class; } sub FETCH { my ($self) = @_; return strftime($self->{format}, localtime); } sub STORE { die "Cannot assign to a timestamp variable\n"; } package main; use feature 'say'; tie my $now, 'Timestamp'; say $now; # 2026-04-03 14:30:00 sleep 2; say $now; # 2026-04-03 14:30:02 (different!) tie my $today, 'Timestamp', '%Y-%m-%d'; say $today; # 2026-04-03
$now always returns the current time. There is no
stored value. FETCH computes it fresh every time. STORE throws
an error because assigning to a timestamp makes no sense.
This is a read-only, self-updating variable. It looks like a constant, but it changes every second.
Part 7: AUTO-INCREMENTING COUNTERS
A variable that increments itself on every read:Every read returns the current value and then bumps it. You get a unique ID generator that looks like a plain variable. No function call syntax. Just read the variable and you get the next number.package AutoIncrement; sub TIESCALAR { my ($class, $start) = @_; return bless { value => ($start // 0) }, $class; } sub FETCH { my ($self) = @_; return $self->{value}++; } sub STORE { my ($self, $value) = @_; $self->{value} = $value; } package main; use feature 'say'; tie my $id, 'AutoIncrement', 1; say $id; # 1 say $id; # 2 say $id; # 3 $id = 100; # reset say $id; # 100 say $id; # 101
Part 8: TYPE-ENFORCED VARIABLES
Build a typed scalar that only accepts values of a specific type:Perl does not have static types. But with tied scalars, you can build runtime type enforcement that feels almost like it does. The variable rejects anything that does not match the expected type at the moment of assignment.package TypedScalar; my %validators = ( int => sub { $_[0] =~ m~^-?\d+$~ }, float => sub { $_[0] =~ m~^-?\d+\.?\d*$~ }, str => sub { defined $_[0] }, email => sub { $_[0] =~ m~^[^@]+@[^@]+\.[^@]+$~ }, ); sub TIESCALAR { my ($class, $type) = @_; die "Unknown type: $type\n" unless $validators{$type}; return bless { value => undef, type => $type }, $class; } sub FETCH { my ($self) = @_; return $self->{value}; } sub STORE { my ($self, $value) = @_; my $type = $self->{type}; my $check = $validators{$type}; die "Invalid $type: '$value'\n" unless $check->($value); $self->{value} = $value; } package main; use feature 'say'; tie my $age, 'TypedScalar', 'int'; tie my $email, 'TypedScalar', 'email'; $age = 25; # fine $email = 'hacker@perl.gg'; # fine $age = "old"; # dies: Invalid int: 'old' $email = "not-an-email"; # dies: Invalid email: ...
Part 9: TIE VS OVERLOAD
Perl has another mechanism for custom behavior:overload. It is
important to understand when to use which.
FEATURE tie overload --------- ----- -------- Applies to variables objects Intercepts read/write operators (+, -, etc.) Granularity per-variable per-class Transparency variable looks normal object looks normal Use case custom storage/access custom arithmetic/compare
tie intercepts variable access. overload intercepts operator
usage on objects. They solve different problems.
Use tie when you want a variable that looks like a normal $x
but has hidden behavior. Use overload when you want objects that
work with +, -, ==, and other operators.
You can combine them if you are feeling ambitious, but that way lies madness.
Part 10: WHEN TO USE TIE
Tied variables are powerful but not free. Every FETCH and STORE is a method call, which is slower than a plain variable access. For a variable you read once in a config, no problem. For a variable inside a tight loop that runs a million times, the overhead adds up.The golden rule: useGOOD USES FOR TIE: * Configuration values with validation * Debug logging during development * Computed/dynamic values (timestamps, counters) * Read-only constants that die on write * Database-backed values BAD USES FOR TIE: * Loop counters in hot paths * Anything where you need raw speed * When a simple function call would be clearer
tie when you need a variable to behave
differently while still looking like a variable. If the calling
code should not know or care that something special is happening,
tie is the right tool.
Tied scalars are one of those Perl features that most programmers never use because they never know it exists. But once you see the pattern, you start noticing places where a variable that validates itself, or logs itself, or computes itself, would eliminate entire classes of bugs..--. |o_o | "$x = 5 just fired |:_/ | three methods and // \ \ logged to a database. (| | ) You're welcome." /'\_ _/`\ \___)=(___/
The interface is tiny. Three methods. The power is enormous. A variable that looks perfectly normal from the outside but does whatever you want on the inside.
That is not a hack. That is a programming language trusting you to redefine what a variable means.
perl.gg