perl.gg / snippets

Tied Scalars: Variables That Spy on Themselves

2026-04-03

This page explains how Perl's tie function lets you attach custom behavior to a plain variable, so reads and writes run your code instead of doing the default.
tie my $temp, 'Celsius'; $temp = 100; say $temp; # 100 say "Boiling!" if $temp >= 100; $temp = -300; # dies: "Temperature below absolute zero"
That code block looks like a normal scalar assignment. It behaves like a normal scalar. Behind the scenes, your custom methods fire every time the variable is read or written. The variable is essentially monitoring itself.

This is tie. Perl's built-in mechanism for making ordinary variables run custom logic on every access.

Part 1: WHAT TIE DOES

The tie function binds a variable to a class. After that binding, every operation on the variable, including reads, writes, and destruction, calls a method on that class instead of running the default behavior.
tie my $var, 'MyClass'; $var = 42; # calls MyClass->STORE($value) my $x = $var; # calls MyClass->FETCH() untie $var; # calls MyClass->UNTIE() then DESTROY()
From the outside, $var is still just a scalar. You can assign to it with =, interpolate it inside strings, and pass it to functions. Nothing in the calling code changes. Every access still routes through your methods.

It works like a transparent proxy sitting in front of the variable. The rest of your program never knows the proxy exists.

Part 2: THE INTERFACE

A tied scalar class needs exactly three methods. That three method set is the complete 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. Perl calls it when you run tie. The object it returns holds the hidden state for that variable.

FETCH runs whenever someone reads the variable. Return whatever the variable should appear to contain.

STORE runs whenever someone writes to the variable. Process the incoming value however you choose.

That is the entire surface area. Three methods give you a custom variable.

Part 3: A MINIMAL EXAMPLE

A variable that counts how many times it has been read:
package 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
The tied function returns the underlying object behind a tied variable. You can call any methods on that object, not just the three tie interface methods.

Part 4: VALIDATION ON WRITE

Reject bad data the moment it touches the variable:
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"
The variable enforces its own contract. You cannot put garbage into it. The validation lives inside the variable itself instead of scattering across every function that touches it.

Part 5: LOGGING EVERY ACCESS

Watch everything that happens to a variable. This setup is useful for debugging mysterious mutations:
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
Every read and write prints the variable name and value to standard error. Attach it during debugging and remove it when you are done. The rest of your code never changes.

Part 6: COMPUTED VALUES

A variable that computes its value on the fly. It never stores anything, and every read produces a fresh result:
package 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
The variable $now always returns the current time. There is no stored value. The FETCH method computes it fresh on every read. The STORE method 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:
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
Every read returns the current value and then bumps it. You get a unique ID generator that looks like a plain variable. There is no function call syntax. You just read the variable and receive the next number.

Part 8: TYPE-ENFORCED VARIABLES

Build a typed scalar that only accepts values of a specific type:
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: ...
Perl does not have static types built into the language. Tied scalars let you build runtime type enforcement that feels close to the real thing. The variable rejects anything that does not match the expected type at the moment of assignment.

Part 9: TIE VS OVERLOAD

Perl has another mechanism for custom behavior called overload. It matters to know when to use which tool.
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 runs hidden logic. Use overload when you want objects to work with +, -, ==, and other operators.

You can combine them if you are feeling ambitious, but that path usually creates more confusion than it solves.

Part 10: WHEN TO USE TIE

Tied variables are powerful, but they are not free. Every FETCH and STORE is a method call, and method calls cost more than plain variable access. A variable you read once in a config file is fine. A variable inside a tight loop that runs a million times will pay that overhead repeatedly.
GOOD 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
The golden rule is to use 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.
.--. |o_o | "$x = 5 just fired |:_/ | three methods and // \ \ logged to a database. (| | ) You're welcome." /'\_ _/`\ \___)=(___/
Tied scalars are one of those Perl features that most programmers never use because they never know it exists. Once you see the pattern, you start noticing places where a variable that validates itself, logs itself, or computes itself would eliminate entire classes of bugs.

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