DEV Community

Cover image for Positional Methods
Elizabeth Mattijsen
Elizabeth Mattijsen

Posted on • Edited on

Positional Methods

This is part nine in the "Cases of UPPER" series of blog posts, describing the Raku syntax elements that are completely in UPPERCASE.

This part will discuss the interface methods that one can implement to provide a custom Positional interface in the Raku Programming Language. Positional access is indicated by the postcircumfix [ ] operator (aka the "array indexing operator").

Some background

The Positional role is really just a marker. It does not enforce any methods to be provided by the consuming class. So why use it? Because it is that constraint that is being checked for any variable with an @ sigil. An example:

class Foo { }
my @a := Foo;
Enter fullscreen mode Exit fullscreen mode

will show an error:

Type check failed in binding; expected Positional but got Foo (Foo)
Enter fullscreen mode Exit fullscreen mode

However, if we make that class ingest the Positional marker role, it works:

class Foo does Positional { }
my @a := Foo;
say @a[0];  # (Foo)
Enter fullscreen mode Exit fullscreen mode

and it is even possible to call the postcircumfix [ ] operator on it!

Note that the binding operator := was used: otherwise the array @a would just be initialized with the Foo type object as its first element.

So why is that possible? That's really possible because each item in Raku can be considered as a single element list. And thus the first element (aka [0]) will always provide the invocant.

say 42[0];  # 42
say 42[1];  # Index out of range. Is: 1, should be in 0..0
Enter fullscreen mode Exit fullscreen mode

Which in turn is why the Positional role does not enforce any methods. Because they are always already provided by the Any class, so it would never complain about methods not having been provided.

Postcircumfix [ ]

The postcircumfix [ ] operator performs all of the work of slicing and dicing objects that perform the Positional role, and handling all of the adverbs: :exists, :delete, :p, :kv, :k, and :v. But it is completely agnostic about how this is actually done, because all it does is calling the interface methods that are (implicitely) provided by the object. For instance:

say 42[0];  # 42
Enter fullscreen mode Exit fullscreen mode

is actually doing:

say 42.AT-POS(0);
Enter fullscreen mode Exit fullscreen mode

under the hood. So these interface methods are the ones that actually know how to work on an Array, a List or a Blob, or any other class that does the Positional role.

The Interface Methods

Let's introduce the cast of this show (the interface methods associated with the Positional role):

AT-POS

The AT-POS method is the most important method of the interface methods: it is expected to take the integer index of the element to be returned, and return that. It should return a container if that is appropriate, which is usually the case. Which means you probably should specify is raw on the AT-POS method if you're implementing that yourself.

say @a[$index];  # same as @a.AT-POS($index)
Enter fullscreen mode Exit fullscreen mode

EXISTS-POS

The EXISTS-POS method is expected to take the integer index of an element, and return a Bool value indicating whether that element is considered to be existing or not. This is what is being called when the :exists adverb is specified.

say @a[$index]:exists;  # same as @a.EXISTS-POS($index)
Enter fullscreen mode Exit fullscreen mode

DELETE-POS

The DELETE-POS method is supposed to act very much like the AT-POS method. But is also expected to remove the element so that the EXISTS-POS method will return False for that element in the future. This is what is being called when the :delete adverb is specified.

say @a[$index]:delete;  # same as @a.DELETE-POS($index)
Enter fullscreen mode Exit fullscreen mode

ASSIGN-POS

The ASSIGN-POS method is a convenience method that may be called when assigning (=) a value to an element. It takes 2 arguments: the index and the value. It functionally defaults to object.AT-POS(index) = value. A typical reason for implementing this method is performance.

say @a[$index] = 42;  # same as @a.ASSIGN-POS($index, 42)
Enter fullscreen mode Exit fullscreen mode

BIND-POS

The BIND-POS method is a method that will be called when binding (:=) a value to an element. It takes 2 arguments: the index and the value. If not implemented, binding will always fail with an execution error.

say @a[$index] := 42;  # same as @a.BIND-POS($index, 42)
Enter fullscreen mode Exit fullscreen mode

STORE

The STORE method accepts the values to be (re-)initializing with as an Iterable and returns the invocant (self).

The :INITIALIZE named argument will be passed with a True value if this is the first time the values are to be set. This is important if your data structure is supposed to be immutable: if that argument is False or not specified, it means a re-initialization is being attempted.

say @a = 42, 666, 137;  # same as @a.STORE( (42, 666, 137) )
Enter fullscreen mode Exit fullscreen mode

elems

Although not an uppercase named method, it is an important interface method: the elems method is expected to return the number of elements in the object. Basically the highest index value + 1 that can be specified expected to return a value (because indexing starts at 0).

Handling simple customizations

Wow, that's a lot to take in! But if it is just a simple customization you wish to do to the basic functionality of e.g. an array, you can simply inherit from the Array class. Here's a simple, contrived example that will return any content of the array doubled as string:

class Array::Twice is Array {
    method AT-POS(Int:D $index) { callsame() x 2 }
}
my @a is Array::Twice = <a b c>;
say "\@a[$_] = @a[$_]" for ^@a;
Enter fullscreen mode Exit fullscreen mode

will show:

@a[0] = aa
@a[1] = bb
@a[2] = cc
Enter fullscreen mode Exit fullscreen mode

Note that in this case the callsame function is called to get the actual value from the array.

Another contrived example where the customization initializes the array with given values in random order (using .pick):

class Array::Confused is Array {
    method STORE(\values) { self.Array::STORE(values.pick(*)) }
}
my @a is Array::Confused = <a b c>;
say "\@a[$_] = @a[$_]" for ^@a;
Enter fullscreen mode Exit fullscreen mode

will show something like:

@a[0] = b
@a[1] = a
@a[2] = c
Enter fullscreen mode Exit fullscreen mode

Note that this example uses the fully qualified method syntax (self.Array::STORE) to set the values in the array.

Helpful modules

If you want to be able to do more than just simple modifications, but for instance have an existing data structure on which you want to provide a Positional interface, it becomes a bit more complicated and potentially cumbersome.

Fortunately there are a number of modules in the ecosystem that will help you creating a consistent Positional interface for your class.

Array::Agnostic

The Array::Agnostic distribution provides an Array::Agnostic role with all of the necessary logic for making your object act as an Array. The only methods that must be provided, are the AT-POS and elems methods. Your class may provide more methods for functionality or performance reasons.

List::Agnostic

The List::Agnostic distribution provides a List::Agnostic role with all of the necessary logic for making your object act as a List. The only methods that you must be provided, are the AT-POS and elems methods. As with Array::Agnostic, your class may provide more methods for functionality or performance reasons.

Example class

A very contrived example in which a Date object is going to represented by a 3-element array. The class looks like:

use List::Agnostic;

class Date::Indexed does List::Agnostic {
    has $!date;
    method AT-POS(Int:D $index) {
        try (*.year, *.month, *.day)[$index]($!date)
    }
    method elems() { 3 }
    method STORE(\values) {
        $!date := Date.new(|values) // Date.today; self
    }
}
Enter fullscreen mode Exit fullscreen mode

and can be used like this:

my @a is Date::Indexed = "2026-02-11";
say "\@a[$_] = @a[$_]" for ^@a;
Enter fullscreen mode Exit fullscreen mode

which would show:

@a[0] = 2026
@a[1] = 2
@a[2] = 11
Enter fullscreen mode Exit fullscreen mode

Note that the STORE method had to be provided by the class to allow for the is Date::Indexed syntax to work.

If you're wondering what's happening with try (*.year, *.month, *.day)[$index]($!date): that's a list built of 3 WhateverCodes with the specified index selecting the appropriate entry and executing that with the $!date attribute as the invocant. If that would fail (probably because the index was out of range), then the try will make sure that the error is caught and a Nil is returned.

Conclusion

This concludes the ninth episode of cases of UPPER language elements in the Raku Programming Language, the second episode discussing interface methods.

In this episode the AT-POS family of methods were described, as well as some simple customizations and some handy Raku modules that can help you create a fully functional interface: List::Agnostic and Array::Agnostic.

Stay tuned for the next episode!

Top comments (0)