Associative Methods
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 ofPOSinKEYwill 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%hwith 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
| Method | Description |
|---|---|
| AT-KEY | The most important method. Takes a key and returns the associated value (usually a container). |
| EXISTS-KEY | Takes a key and returns a Bool indicating whether the key exists. |
| DELETE-KEY | Takes a key, returns the associated value, and removes the key so that EXISTS-KEY will return False thereafter. |
| ASSIGN-KEY | Convenience method called when assigning (=) a value to a key. Takes two arguments (key, value) and returns the value. |
| BIND-KEY | Called when binding (:=) a value to a key. Takes two arguments (key, value) and returns the value. |
| STORE | Accepts an Iterable of values with which to (re‑)initialize the hash, and returns the invocant. Receives a named argument :INITIALIZE (True on first initialization). |
| keys | Not 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.
HashandMapcoerce the key to a string internally. - The method should return a container when appropriate, which usually means you should specify
is rawon 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
:INITIALIZEnamed argument will beTrueon 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::Agnosticrole 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
STOREmethod is required so that theis Hash::Intsyntax works.
How STORE Works
Map.CREATE.STORE(values, :INITIALIZE) leverages the initialization logic of hashes and maps, which accepts:
- Separate
key, valuepairs, key => valuepairs,- 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-KEYfamily of methods, - Simple customizations,
- Handy Raku modules that help you build a fully functional interface:
Map::AgnosticandHash::Agnostic.
Stay tuned for the next episode!