Associative Methods

Published: (February 12, 2026 at 02:28 PM EST)
6 min read
Source: Dev.to

Source: Dev.to

Cases of UPPER – Part 10

Custom Associative Interface (Raku)

This is part 10 in the Cases of UPPER series of blog posts, describing the Raku syntax elements that are completely in UPPERCASE.
In this post we discuss the interface methods that can be implemented to provide a custom Associative interface in the Raku Programming Language. Associative access is indicated by the post‑circumfix { } operator (aka the “hash indexing operator”).

In a way this blog post is a cat license (“a dog licence with the word dog crossed out and cat written in crayon”) from the previous post.
But there are subtle differences between the Positional and the Associative roles, so simply changing all occurrences of POS in KEY will not cut it.


The Associative role

The Associative role is really just a marker, just as the Positional role. It does not enforce any methods to be provided by the consuming class.

So why use it? Because the constraint is checked for any variable with a % sigil.

class Foo { }
my %h := Foo;   # ← type‑check error
Type check failed in binding; expected Associative but got Foo (Foo)

If the class does the Associative marker role, it works:

class Foo does Associative { }
my %h := Foo;
say %h;    # (Any)

It is even possible to call the post‑circumfix { } operator on it, although it doesn’t return anything particularly useful.

Note: The binding operator := was used; otherwise it would be interpreted as initializing a hash %h with a single value, which would raise an “Odd number of elements found where hash initializer expected” error.

The post‑circumfix { } operator performs all of the work of slicing and dicing objects that perform the Associative role, handling all of the adverbs :exists, :delete, :p, :kv, :k, and :v. It is completely agnostic about how this is done—it simply calls the interface methods that are (implicitly) provided by the object, just as with the Positional role. The method names are different, though. For instance:

say %h;

is actually doing:

say %h.AT-KEY("bar");

under the hood. These interface methods are the ones that actually know how to work on a Hash, Map, PseudoStash, or any other class that does the Associative role.


Interface methods associated with the Associative role

MethodDescription
AT-KEYThe most important method. Takes a key and returns the associated value (usually a container).
EXISTS-KEYTakes a key and returns a Bool indicating whether the key exists.
DELETE-KEYTakes a key, returns the associated value, and removes the key so that EXISTS-KEY will return False thereafter.
ASSIGN-KEYConvenience method called when assigning (=) a value to a key. Takes two arguments (key, value) and returns the value.
BIND-KEYCalled when binding (:=) a value to a key. Takes two arguments (key, value) and returns the value.
STOREAccepts an Iterable of values with which to (re‑)initialize the hash, and returns the invocant. Receives a named argument :INITIALIZE (True on first initialization).
keysNot an uppercase method, but an important part of the interface. Returns the keys of the object.

AT-KEY

say %h;          # same as %h.AT-KEY("bar")
  • The key does not need to be a string; any object can be used. Hash and Map coerce the key to a string internally.
  • The method should return a container when appropriate, which usually means you should specify is raw on the method if you implement it yourself.

EXISTS-KEY

say %h:exists;   # same as %h.EXISTS-KEY("bar")

DELETE-KEY

say %h:delete;   # same as %h.DELETE-KEY("bar")

ASSIGN-KEY

say %h = 42;     # same as %h.ASSIGN-KEY("bar", 42)
  • A typical reason for implementing this method is performance.

BIND-KEY

say %h := 42;    # same as %h.BIND-KEY("bar", 42)

STORE

%h = a => 42, b => 666;   # same as %h.STORE( (a => 42, b => 666) )
  • The :INITIALIZE named argument will be True on the first call, allowing immutable structures to reject later re‑initializations.

keys

my %h = a => 42, b => 666;
say %h.keys;               # (a b)

Simple customisation example

If you only need a simple customisation of the basic hash functionality, you can inherit from Hash:

class Hash::Twice is Hash {
    method AT-KEY($key) { callsame() x 2 }
}

my %h is Hash::Twice = a => 42, b => 666;
say "$_: %h{$_}" for %h.keys;

Output

a: 4242
b: 666666

Note: callsame retrieves the original value before it is doubled.


More complex cases

When you want to expose an existing data structure via an Associative interface, things become a bit more involved. Fortunately, several modules in the ecosystem can help you create a consistent interface.

  • Hash::Agnostic – provides a Hash::Agnostic role with all the necessary logic for making your object act as a hash. The only methods you must supply are the ones listed above.

(…the post continues with further details on using Hash::Agnostic and other modules…)

AT‑KEY and keys Methods

The Map::Agnostic role provides all the logic needed for an object to behave like a Map.
The only methods you must implement are AT-KEY and keys.
As with Hash::Agnostic, you may add extra methods for functionality or performance.


A Contrived Example: Hash::Int

Below is a minimal Hash::Int class that implements an associative interface whose keys are only integers. Internally it stores values in an array.

use Hash::Agnostic;

class Hash::Int does Hash::Agnostic {
    has @!values;

    method AT-KEY(Int:D $index) {
        @!values.AT-POS($index)
    }

    method keys() {
        (^@!values).grep: { @!values.EXISTS-POS($_) }
    }

    method STORE(\values) {
        my @values;
        for Map.CREATE.STORE(values, :INITIALIZE) {
            @values.ASSIGN-POS(.key.Int, .value)
        }
        @!values := @values;
    }
}

Using the class

my %h is Hash::Int = ;
say %h;
dd %h;

Output:

a
Hash::Int.new(42 => "a", 137 => "c", 666 => "b")

Note: The STORE method is required so that the is Hash::Int syntax works.


How STORE Works

Map.CREATE.STORE(values, :INITIALIZE) leverages the initialization logic of hashes and maps, which accepts:

  • Separate key, value pairs,
  • key => value pairs,
  • Any mixture of the above.

The call produces a consistent Seq of Pairs that we then use to populate the underlying array.


Built‑in Alternative

Raku already provides a syntax for a hash that only accepts Int keys:

my %h{Int}

This creates an object hash with different performance characteristics than the custom Hash::Int shown above.


Closing Remarks

This concludes the tenth episode of “Cases of UPPER Language Elements” in the Raku Programming Language series, the third episode focusing on interface methods.

In this episode we covered:

  • The AT-KEY family of methods,
  • Simple customizations,
  • Handy Raku modules that help you build a fully functional interface: Map::Agnostic and Hash::Agnostic.

Stay tuned for the next episode!

0 views
Back to Blog

Related posts

Read more »

shadcn & ai give me superpower....

While working on the frontend of my project, I used shadcn/ui, and it has been a great experience. The components are clean, stable, and highly customizable. Si...

Partial Indexes in PostgreSQL

Partial indexes are refined indexes that target specific access patterns. Instead of indexing every row in a table, they only index the rows that match a condit...