Store Proxy Fetch
Source: Dev.to
Cases of UPPER – Part 12
Containers in Raku
This is part 12 of the Cases of UPPER series, which documents the Raku syntax elements that are written completely in UPPERCASE.
In this post we look at containers – the objects that actually hold values in Raku.
Binding vs. Assignment
Raku really only knows about binding (:=).
Assignment (=) is just syntactic sugar that ultimately performs a bind.
What?
Most scalar variables you see in Raku are objects that contain an attribute to which the value is bound when you assign to the variable. These objects are called Scalar containers (or simply containers).
In short: assignment in Raku is binding to an attribute in a Scalar object.
Understanding containers is a rite of passage for anyone who wants to become an efficient Raku developer – it was for me, too!
A (very) simplified model
# Pseudocode – not real Raku
class Scalar {
has $!value;
method STORE($new-value) {
$!value := $new-value;
}
method FETCH() {
$!value;
}
}
An assignment such as
my $a;
$a = 42;
can be thought of as
my $a := Scalar.new; # bind $a to a new container
$a.STORE(42); # store the value
and printing the variable (say $a) is equivalent to
say $a.FETCH;
In reality things are a bit more complex because:
- Type constraints (
my Int $a) may be attached to a variable. - Variables can have default values (
my $a is default(42)).
All that extra information lives in a container descriptor – an implementation detail that you should not rely on directly, because its interface may change.
Why the indirection?
Assignments are among the most frequently executed operations, so Raku implements them with many shortcuts for performance.
Having a mental model of containers helps you understand a surprising number of language constructs.
Returning containers (the rw trait)
Values returned from a block (or via return) are de‑containerized by default:
my $a = 42;
sub foo() { $a } # returns the *value* 42
foo() = 666; # ❌ cannot assign to an immutable Int
If you need to return the container itself, you have two easy options.
1. is rw trait
my $a = 42;
sub foo() is rw { $a } # returns the container
foo() = 666; # works
say $a; # 666
2. return-rw statement
my $a = 42;
sub foo() { return-rw $a }
foo() = 666;
say $a; # 666
These mechanisms are used, for example, when assigning to an array element.
In the core, both the postcircumfix […] operator and the underlying AT-POS method have the is rw trait for speed (the return-rw statement adds a tiny bit of overhead).
Containers in arrays and hashes
When an array is initialized with values, each element gets its own container.
You can fetch values, store new ones, or bind a container to another variable.
my @a = 1, 2, 3, 4, 5;
$_++ for @a; # increment each element
say @a; # [2 3 4 5 6]
- The topic variable
$_is bound to each element’s container during the loop, so$_++actually callsSTOREon that container.
Note:
$_is not a pointer or reference to a memory location. It is bound to the container object, which knows how toFETCHandSTORE.
Slices also return containers
my @a = 1, 2, 3, 4, 5;
$_++ for @a[0, *-1]; # first and last element only
say @a; # [2 2 3 4 6]
The slice [0, *-1] produces two containers (first and last), and only those are mutated.
The same idea works for hashes:
my %h = a => 42, b => 666;
$_++ for %h.values; # increment each value
say %h; # {a => 43, b => 667}
Direct binding in user code
You can bind one scalar to another explicitly:
my $a = 42;
my $b := $a; # $b is bound to $a’s container
$b = 666; # stores into the same container
say $a; # 666
Assigning to $b actually stores into the container that lives inside $a.
Assigning to non‑existent array elements
Raku lets you assign to an array index that does not yet exist:
my @a;
@a[3] = 42;
say @a; # [(Any) (Any) (Any) 42]
Even though there is no container at index 3 initially, the assignment creates one on the fly.
The secret again lies in the container descriptor.
Let’s refine our pseudocode to include it:
# Pseudocode – not real Raku
class Scalar {
has $!descriptor; # holds name, type, defaults, etc.
has $!value;
method STORE($value) {
$!value := $!descriptor.process($value);
}
method FETCH() {
$!value;
}
}
Now STORE delegates to $!descriptor.process($value), which can:
- Remember the variable’s name.
- Enforce type constraints.
- Supply a default value.
- Perform any other bookkeeping required by the container.
Take‑away
- Containers are the heart of Raku’s variable model.
- Assignment (
=) is just sugar for binding (:=). - The
is rwtrait andreturn‑rwlet you return a container instead of its value. - Arrays, hashes, slices, and even loop topics work by binding to containers.
- The container descriptor handles type checking, defaults, and other metadata.
Understanding these concepts will make many “magic‑looking” Raku features click into place, and will give you the confidence to write fast, idiomatic code. Happy container‑binding!
Containers and Proxies in Raku
There are many different types of container descriptor classes in the Rakudo implementation, all starting with the ContainerDescriptor:: name. All of them are considered implementation details.
Array and Hash Descriptors
When an AT‑POS method is called on a non‑existing element in an array, a container with a special type of descriptor is created that knows to which Array it belongs and at which index it should be stored.
The same is true for the descriptor of the container returned by AT‑KEY (as seen in part 10); it knows in which Hash it should store when assigned to, and which key should be used.
This special behaviour allows arrays and hashes to really just DWIM.
These descriptors also introduce an action‑at‑a‑distance feature that you may or may not like:
my @a;
my $b := @a[3];
say @a; # []
$b = 42;
say @a; # [(Any) (Any) (Any) 42]
Note: The element in the array was not initialized after the binding, but only after a value was assigned to
$b. This behaviour was deliberately implemented to prevent accidental auto‑vivification.
Preventing Accidental Auto‑Vivification
my %h;
say %h:exists; # False
say %h; # {}
%h = 42;
say %h; # {a => {b => 42}}
Even though %h is considered an Associative, the :exists test will not create a Hash in %h. Only after a value has been assigned do %h and its nested structures actually spring to life.
The Proxy Class
Raku supplies a fully customisable class that lets you create your own container logic: Proxy. Understanding how containers work is helpful when working with this class.
Creating a Simple Proxy
All you need to supply are:
- a method for fetching the value (
FETCH), and - a method for storing a value (
STORE).
These are passed as named arguments to Proxy.new. A typical pattern is to wrap the creation in a subroutine for convenience:
sub answer is rw {
my $value = 42;
Proxy.new(
FETCH => method () {
$value
},
STORE => method ($new) {
say "storing $new";
$value = $new;
}
)
}
my $a := answer; # bind, not assign
say $a; # 42
$a = 666; # storing 666
say $a; # 666
- The
is rwtrait on the subroutine is required; otherwise theProxywould be de‑containerised on return. - The result of calling
answeris bound (:=) rather than assigned; assignment would also cause de‑containerisation and defeat the purpose.
Because the supplied methods close over the lexical variable $value, that variable stays alive until the Proxy object is destroyed, providing an easy way to store the value for the proxy.
Flexibility Inside FETCH/STORE
Inside the supplied methods you are free to execute any code you like. For example, the Hash::MutableKeys distribution uses this capability (another case of BDD – Blog Driven Development).
If you need a descriptor for your Proxy objects, you can create one yourself; the default Proxy does not include a descriptor because it aims to give you maximum flexibility.
Episode Summary
This is the twelfth episode of “Cases of UPPER Language Elements in the Raku Programming Language,” the fifth episode discussing interface methods.
- Containers were described, including the special descriptors for arrays and hashes.
- The
Proxyclass was introduced, with itsFETCHandSTOREnamed arguments, showing how to build fully custom containers.
Stay tuned for the next episode!