Classes and objects
Raku provides a rich built-in syntax for defining and using classes. It makes writing classes expressive and short for most cases, but also provides mechanisms to cover the rare corner cases.
A quick overview
Let's start with an example to give an overview:
class Rectangle {
has Int $.length = 1;
has Int $.width = 1;
method area(--> Int) {
return $!length * $!width;
}
}
my $r1 = Rectangle.new(length => 2, width => 3);
say $r1.area(); # OUTPUT: «6»
We define a new Rectangle class using the class keyword. It has two attributes, $!length
and $!width
introduced
with the has keyword. Both default to 1
. Read only accessor methods are automatically generated. (Note the .
instead of !
in the declaration, which triggers the generation. Mnemonic: !
resembles a closed door, .
an open one.)
The method named area
will return the area of the rectangle.
It is rarely necessary to explicitly write a constructor. An automatically inherited default constructor called new will automatically initialize attributes from named parameters passed to the constructor.
The Task example
As a more elaborate example the following piece of code implements a dependency handler. It showcases custom constructors, private and public attributes, methods, and various aspects of signatures. It's not a lot of code, and yet the result is interesting and useful. It will be used as an example throughout the following sections.
class Task {
has &!callback is built;
has Task @!dependencies is built;
has Bool $.done;
method new(&callback, *@dependencies) {
return self.bless(:&callback, :@dependencies);
}
method add-dependency(Task $dependency) {
push @!dependencies, $dependency;
}
method perform() {
unless $!done {
.perform() for @!dependencies;
&!callback();
$!done = True;
}
}
}
my $eat =
Task.new({ say 'eating dinner. NOM!' },
Task.new({ say 'making dinner' },
Task.new({ say 'buying food' },
Task.new({ say 'making some money' }),
Task.new({ say 'going to the store' })
),
Task.new({ say 'cleaning kitchen' })
)
);
$eat.perform();
Class
Raku, like many other languages, uses the class
keyword to define a
class. The block that follows may contain arbitrary code, just as with
any other block, but classes commonly contain state and behavior
declarations. The example code includes attributes (state), introduced
through the has
keyword, and behaviors, introduced through the method
keyword.
Attributes
In the Task
class, the first three lines inside the block all
declare attributes (called fields or instance storage in other
languages) using the has
declarator. Just as a my
variable cannot be
accessed from outside its declared scope, attributes are never directly
accessible from outside of the class (this is in contrast to many other
languages). This encapsulation is one of the key principles of object
oriented design.
Twigil $!
|Tutorial,twigils;Tutorial,!;Tutorial,&
The first declaration specifies instance storage for a callback (i.e. a bit of code to invoke in order to perform the task that an object represents):
has &!callback is built;
The &
sigil indicates that this attribute represents something invocable.
The !
character is a twigil, or secondary sigil. A twigil forms part
of the name of the variable. In this case, the !
twigil emphasizes that
this attribute is private to the class. The attribute is encapsulated.
Private attributes will not be set by the default constructor by default, which is
why we add the is built
trait to allow just that. Mnemonic: !
looks like a closed door.
The second declaration also uses the private twigil:
has Task @!dependencies is built;
However, this attribute represents an array of items, so it requires the
@
sigil. These items each specify a task that must be completed before
the present one is completed. Furthermore, the type declaration on this
attribute indicates that the array may only hold instances of the Task
class (or some subclass of it).
Twigil $.
The third attribute represents the state of completion of a task:
has Bool $.done;
This scalar attribute (with the $
sigil) has a type of Bool. Instead
of the !
twigil, the .
twigil is used. While Raku does enforce
encapsulation on attributes, it also saves you from writing accessor
methods. Replacing the !
with a .
both declares a private attribute
and an accessor method named after the attribute. In this case,
both the attribute $!done
and the accessor method done
are declared.
It's as if you had written:
has Bool $!done;
method done() { return $!done }
Note that this is not like declaring a public attribute, as some languages allow; you really get both a private attribute and a method, without having to write the method by hand. You are free instead to write your own accessor method, if at some future point you need to do something more complex than returning the value.
is rw
trait|Tutorial,is rw
Note that using the .
twigil has created a method that will provide
read-only access to the attribute. If instead the users of this object
should be able to reset a task's completion state (perhaps to perform it
again), you can change the attribute declaration:
has Bool $.done is rw;
The is rw
trait causes the generated accessor method to return a container
so external code can modify the value of the attribute.
is built
trait
has &!callback is built;
By default private attributes are not automatically set by the default constructor. (They are private after
all.) In the above example we want to allow the user to provide the initial value but keep the attribute
otherwise private. The is built
trait allows to do just that.
One can also use it to do the opposite for public attributes, i.e. prevent them to be automatically initialized with a user provided value, but still generate the accessor method:
has $.done is built(False);
Above declaration makes sure one can't construct finished tasks, but still allow users to look if a task is done.
The is built
trait was introduced in Rakudo version 2020.01.
is required
trait
Providing a value for an attribute during initialization is optional by default. Which in the task example
makes sense for all three, the &!callback
, the @!dependencies
and the $.done
attribute. But lets say
we want to add another attribute, $.name
, that holds a tasks name and we want to force the user to
provide a value on initialization. We can do that as follows:
has $.name is required;
Default values
You can also supply default values to attributes (which works equally for those with and without accessors):
has Bool $.done = False;
The assignment is carried out at object build time. The right-hand side is evaluated at that time, and can even reference earlier attributes:
has Task @!dependencies;
has $.ready = not @!dependencies;
Writable attributes are accessible through writable containers:
class a-class {
has $.an-attribute is rw;
}
say (a-class.new.an-attribute = "hey"); # OUTPUT: «hey»
This attribute can also be accessed using the .an-attribute
or
.an-attribute()
syntax. See also
the is rw trait on classes for examples
on how this works on the whole class.
Class variables
A class declaration can also include class variables, declared with my
or our
, which are variables
whose value is shared by all instances, and can be used for things like
counting the number of instantiations or any other shared state. So class variables act similarly to
static variables known from other programming languages.
They look the same as normal (non class) lexical variables (and in fact they are the same):
class Str-with-ID is Str {
my $counter = 0;
our $our-counter = 0;
has Str $.string;
has Int $.ID is built(False);
submethod TWEAK() {
$counter++;
$our-counter++;
$!ID = $counter;
}
}
class Str-with-ID-and-tag is Str-with-ID {
has Str $.tag;
}
say Str-with-ID.new(string => 'First').ID; # OUTPUT: «1»
say Str-with-ID.new(string => 'Second').ID; # OUTPUT: «2»
say Str-with-ID-and-tag.new( string => 'Third', tag => 'Ordinal' ).ID; # OUTPUT: «3»
say $Str-with-ID::our-counter; # OUTPUT: «3»
Class variables are shared by all subclasses, in this case
Str-with-ID-and-tag
. Additionally, when the package scope our
declarator
is used, the variable is visible via their fully qualified name (FQN), while
lexically scoped my
variables are "private". This is the exact behavior that
my
and our
also show in non class context.
Class variables act similarly to static variables in many other programming languages.
class Singleton {
my Singleton $instance;
method new {!!!}
submethod instance {
$instance = Singleton.bless unless $instance;
$instance;
}
}
In this implementation of the Singleton pattern a class variable is used to save the instance.
class HaveStaticAttr {
my Int $.foo = 5;
}
Class attributes may also be declared with a secondary sigil – in a similar manner to instance attributes – that will generate read-only accessors if the attribute is to be public. Default values behave as expected and are assigned only once.
Methods
While attributes give objects state, methods give objects behaviors. Back to our Task
example. Let's
ignore the new
method temporarily; it's a special type of method.
Consider the second method, add-dependency
, which adds a new task to a
task's dependency list:
method add-dependency(Task $dependency) {
push @!dependencies, $dependency;
}
In many ways, this looks a lot like a sub
declaration. However, there are
two important differences. First, declaring this routine as a method adds it
to the list of methods for the current class, thus any instance of the
Task
class can call it with the .
method call operator.
Second, a method places its invocant into the special variable self
.
The method itself takes the passed parameter – which must be an instance of
the Task
class – and push
es it onto the invocant's @!dependencies
attribute.
The perform
method contains the main logic of the dependency handler:
method perform() {
unless $!done {
.perform() for @!dependencies;
&!callback();
$!done = True;
}
}
It takes no parameters, working instead with the object's attributes. First,
it checks if the task has already completed by checking the $!done
attribute. If so, there's nothing to do.
Otherwise, the method performs all of the task's dependencies, using the
for
construct to iterate over all of the items in the @!dependencies
attribute. This iteration places each item – each a Task
object – into
the topic variable, $_
. Using the .
method call operator without
specifying an explicit invocant uses the current topic as the invocant.
Thus the iteration construct calls the .perform()
method on every Task
object in the @!dependencies
attribute of the current invocant.
After all of the dependencies have completed, it's time to perform the
current Task
's task by invoking the &!callback
attribute directly;
this is the purpose of the parentheses. Finally, the method sets the
$!done
attribute to True
, so that subsequent invocations of perform
on this object (if this Task
is a dependency of another Task
, for
example) will not repeat the task.
Private methods
Just like attributes, methods can also be private. Private methods are declared
with a prefixed exclamation mark. They are called with self!
followed by the
method's name. In the following implementation of a MP3TagData
class to
extract ID3v1 metadata from an mp3 file,
methods parse-data
, can-read-format
, and trim-nulls
are private
methods while the remaining ones are public methods:
class MP3TagData {
has $.filename where { .IO ~~ :e };
has Str $.title is built(False);
has Str $.artist is built(False);
has Str $.album is built(False);
has Str $.year is built(False);
has Str $.comment is built(False);
has Int $.genre is built(False);
has Int $.track is built(False);
has Str $.version is built(False);
has Str $.type is built(False) = 'ID3';
submethod TWEAK {
with $!filename.IO.open(:r, :bin) -> $fh {
$fh.seek(-128, SeekFromEnd);
my $tagdata = $fh.read(128);
self!parse-data: $tagdata;
$fh.close;
}
else {
warn "Failed to open file."
}
}
method !parse-data($data) {
if self!can-read-format($data) {
my $offset = $data.bytes - 128;
$!title = self!trim-nulls: $data.subbuf($offset + 3, 30);
$!artist = self!trim-nulls: $data.subbuf($offset + 33, 30);
$!album = self!trim-nulls: $data.subbuf($offset + 63, 30);
$!year = self!trim-nulls: $data.subbuf($offset + 93, 4);
my Int $track-flag = $data.subbuf($offset + 97 + 28, 1).Int;
$!track = $data.subbuf($offset + 97 + 29, 1).Int;
($!version, $!comment) = $track-flag == 0 && $!track != 0
?? ('1.1', self!trim-nulls: $data.subbuf($offset + 97, 28))
!! ('1.0', self!trim-nulls: $data.subbuf($offset + 97, 30));
$!genre = $data.subbuf($offset + 97 + 30, 1).Int;
}
}
method !can-read-format(Buf $data --> Bool) {
self!trim-nulls($data.subbuf(0..2)) eq 'TAG'
}
method !trim-nulls(Buf $data --> Str) {
$data.decode('utf-8').subst(/\x[0000]+/, '')
}
}
To call a private method of another class, the caller has to be trusted by the callee. A trust relationship is declared with trusts and the class to be trusted must already be declared. Calling a private method of another class requires an instance of that class and the fully qualified name (FQN) of the method. A trust relationship also allows access to private attributes.
class B {...}
class C {
trusts B;
has $!hidden = 'invisible';
method !not-yours () { say 'hidden' }
method yours-to-use () {
say $!hidden;
self!not-yours();
}
}
class B {
method i-am-trusted () {
my C $c.=new;
$c!C::not-yours();
}
}
C.new.yours-to-use(); # the context of this call is GLOBAL, and not trusted by C
B.new.i-am-trusted();
Trust relationships are not subject to inheritance. To trust the global
namespace, the pseudo package GLOBAL
can be used.
Construction
The object construction mechanisms described up to now suffice for most use cases. But if one actually needs to tweak object construction more than said mechanisms allow, it's good to understand how object construction works in more detail.
Raku is rather more liberal than many languages in the area of
constructors. A constructor is anything that returns an instance of the
class. Furthermore, constructors are ordinary methods. You inherit a
default constructor named new
from the base class Mu, but you are
free to override new
, as the Task example does:
method new(&callback, *@dependencies) {
return self.bless(:&callback, :@dependencies);
}
bless
The biggest difference between constructors in Raku and constructors in
languages such as C# and Java is that rather than setting up state on a
somehow already magically created object, Raku constructors create the
object themselves. They do this by calling the
bless method, also inherited from Mu.
The bless
method expects a set of named parameters to provide the initial
values for each attribute.
The example's constructor turns positional arguments into named arguments,
so that the class can provide a nicer constructor for its users. The first
parameter is the callback (the thing which will execute the task). The rest
of the parameters are dependent Task
instances. The constructor captures
these into the @dependencies
slurpy array and passes them as named
parameters to bless
(note that :&callback
uses the name of the
variable – minus the sigil – as the name of the parameter).
One should refrain from putting logic other than reformulating the parameters in the constructor, because
constructor methods are not recursively called for parent classes. This is different from e.g. Java.
Declaring new
as a method
and not as a multi method
prevents access to the default constructor.
+So if you intend to keep the default constructor available, use multi method new
.
TWEAK
|Tutorial,TWEAK
After bless
has initialized the classes attributes from the passed values, it will in turn call TWEAK
for each class in the inheritance hierarchy.
TWEAK
gets passed all the arguments passed to bless.
This is where custom initialization logic should go.
Remember to always make TWEAK
a submethod and not a normal method
. If in a class hierarchy a class contains a TWEAK
method (declared as a method
instead of a submethod
) that method is inherited to its subclass and will thus be called twice during construction of the subclass!
BUILD
|Tutorial,BUILD
It is possible to disable the automatic attribute initialization and perform the initialization of
attributes oneself. To do so one needs to write a custom BUILD
submethod. There are several edge cases one
needs to be aware of and take into account though. This is detailed in the
Object Construction Reference. Because of the difficulty of using
BUILD
, it is recommended to only make use of it when none of the other approaches described above suffices.
Destruction
Raku is a garbage collecting language. This means that one usually doesn't need to care about cleaning up objects, because Raku does so automatically. Raku does not give any guarantees as to when it will clean up a given object though. It usually does a cleanup run only if the runtime needs the memory, so we can't rely on when it's going to happen.
To run custom code when an object is cleaned up one can use the DESTROY
submethod. It can for example be used to close handles or supplies or delete temporary files that are no longer
going to be used. As garbage collection can happen at arbitrary points during the runtime of our program, even in the middle of some totally unrelated piece of code in a different thread, we
must make sure to not assume any context in our DESTROY
submethod.
my $in_destructor = 0;
class Foo {
submethod DESTROY { $in_destructor++ }
}
my $foo;
for 1 .. 6000 {
$foo = Foo.new();
}
say "DESTROY called $in_destructor times";
This might print something like DESTROY called 5701 times
and possibly only kicks
in after we have stomped over former instances of Foo
a few thousand times. We also can't
rely, on the order of destruction.
Same as TWEAK
: Make sure to always declare DESTROY
as a submethod
.
Consuming our class
After creating a class, you can create instances of the class. Declaring a custom constructor provides a simple way of declaring tasks along with their dependencies. To create a single task with no dependencies, write:
my $eat = Task.new({ say 'eating dinner. NOM!' });
An earlier section explained that declaring the class Task
installed a
type object in the namespace. This type object is a kind of "empty
instance" of the class, specifically an instance without any state. You can
call methods on that instance, as long as they do not try to access any
state; new
is an example, as it creates a new object rather than
modifying or accessing an existing object.
Unfortunately, dinner never magically happens. It has dependent tasks:
my $eat =
Task.new({ say 'eating dinner. NOM!' },
Task.new({ say 'making dinner' },
Task.new({ say 'buying food' },
Task.new({ say 'making some money' }),
Task.new({ say 'going to the store' })
),
Task.new({ say 'cleaning kitchen' })
)
);
Notice how the custom constructor and the sensible use of whitespace makes task dependencies clear.
Finally, the perform
method call recursively calls the perform
method
on the various other dependencies in order, giving the output:
making some money
going to the store
buying food
cleaning kitchen
making dinner
eating dinner. NOM!
A word on types
Declaring a class creates a new type object which, by default, is installed
into the current package (just like a variable declared with our
scope).
This type object is an "empty instance" of the class. For example, types such as
Int and Str refer to the type object of one of the Raku built-in
classes. One can call methods on these type objects. So there is nothing special
with calling the new
method on a type object.
You can use the .DEFINITE
method to find out if what you have is an instance
or a type object:
say Int.DEFINITE; # OUTPUT: «False» (type object)
say 426.DEFINITE; # OUTPUT: «True» (instance)
class Foo {};
say Foo.DEFINITE; # OUTPUT: «False» (type object)
say Foo.new.DEFINITE; # OUTPUT: «True» (instance)
In function signatures one can use so called type "smileys" to only accept instances or type objects:
multi foo (Int:U) { "It's a type object!" }
multi foo (Int:D) { "It's an instance!" }
say foo Int; # OUTPUT: «It's a type object!»
say foo 42; # OUTPUT: «It's an instance!»
Inheritance
Object Oriented Programming provides the concept of inheritance as one of
the mechanisms for code reuse. Raku supports the ability for one
class to inherit from one or more classes. When a class inherits from
another class it informs the method dispatcher to follow the inheritance
chain to look for a method to dispatch. This happens both for standard
methods defined via the method
keyword and for methods generated through
other means, such as attribute accessors.
class Employee {
has $.salary;
}
class Programmer is Employee {
has @.known_languages is rw;
has $.favorite_editor;
method code_to_solve( $problem ) {
return "Solving $problem using $.favorite_editor in "
~ $.known_languages[0];
}
}
Now, any object of type Programmer can make use of the methods and accessors defined in the Employee class as though they were from the Programmer class.
my $programmer = Programmer.new(
salary => 100_000,
known_languages => <Raku Perl Erlang C++>,
favorite_editor => 'vim'
);
say $programmer.code_to_solve('halting problem'),
" will get \$ {$programmer.salary()}";
# OUTPUT: «Solving halting problem using vim in Raku will get $100000»
Overriding inherited methods
Classes can override methods and attributes defined by parent
classes by defining their own. The example below demonstrates the Baker
class overriding the Cook
's cook
method.
class Cook is Employee {
has @.utensils is rw;
has @.cookbooks is rw;
method cook( $food ) {
say "Cooking $food";
}
method clean_utensils {
say "Cleaning $_" for @.utensils;
}
}
class Baker is Cook {
method cook( $confection ) {
say "Baking a tasty $confection";
}
}
my $cook = Cook.new(
utensils => <spoon ladle knife pan>,
cookbooks => 'The Joy of Cooking',
salary => 40000
);
$cook.cook( 'pizza' ); # OUTPUT: «Cooking pizza»
say $cook.utensils.raku; # OUTPUT: «["spoon", "ladle", "knife", "pan"]»
say $cook.cookbooks.raku; # OUTPUT: «["The Joy of Cooking"]»
say $cook.salary; # OUTPUT: «40000»
my $baker = Baker.new(
utensils => 'self cleaning oven',
cookbooks => "The Baker's Apprentice",
salary => 50000
);
$baker.cook('brioche'); # OUTPUT: «Baking a tasty brioche»
say $baker.utensils.raku; # OUTPUT: «["self cleaning oven"]»
say $baker.cookbooks.raku; # OUTPUT: «["The Baker's Apprentice"]»
say $baker.salary; # OUTPUT: «50000»
Because the dispatcher will see the cook
method on Baker
before it
moves up to the parent class the Baker
's cook
method will be called.
To access methods in the inheritance chain, use re-dispatch or the MOP.
Multiple inheritance
As mentioned before, a class can inherit from multiple classes. When a class inherits from multiple classes the dispatcher knows to look at both classes when looking up a method to search for. Raku uses the C3 algorithm to linearize multiple inheritance hierarchies, which is better than depth-first search for handling multiple inheritance.
class GeekCook is Programmer is Cook {
method new( *%params ) {
push( %params<cookbooks>, "Cooking for Geeks" );
return self.bless(|%params);
}
}
my $geek = GeekCook.new(
books => 'Learning Raku',
utensils => ('stainless steel pot', 'knife', 'calibrated oven'),
favorite_editor => 'MacVim',
known_languages => <Raku>
);
$geek.cook('pizza');
$geek.code_to_solve('P =? NP');
Now all the methods made available to the Programmer and the Cook classes are available from the GeekCook class.
While multiple inheritance is a useful concept to know and occasionally use, it is important to understand that there are more useful OOP concepts. When reaching for multiple inheritance it is good practice to consider whether the design wouldn't be better realized by using roles, which are generally safer because they force the class author to explicitly resolve conflicting method names. For more information on roles, see Roles.
The also
|Tutorial,also declarator declarator
Classes to be inherited from can be listed in the class declaration body by
prefixing the is
trait with also
. This also works for the role
composition trait does
.
class GeekCook {
also is Programmer;
also is Cook;
# ...
}
role A {};
role B {};
class C {
also does A;
also does B;
# ...
}
Introspection
Introspection is the process of gathering information about some objects in your program, not by reading the source code, but by querying the object (or a controlling object) for some properties, such as its type.
Given an object $o
and the class definitions from the previous sections,
we can ask it a few questions:
my Programmer $o .= new;
if $o ~~ Employee { say "It's an employee" };
say $o ~~ GeekCook ?? "It's a geeky cook" !! "Not a geeky cook";
say $o.^name;
say $o.raku;
say $o.^methods(:local)».name.join(', ');
The output might look like this:
It's an employee
Not a geeky cook
Programmer
Programmer.new(known_languages => ["Perl", "Python", "Pascal"],
favorite_editor => "gvim", salary => "too small")
code_to_solve, known_languages, favorite_editor
The first two tests each smartmatch against a class name. If the object is
of that class, or of an inheriting class, it returns True
. So the object in
question is of class Employee
or one that inherits from it, but not
GeekCook
.
The call $o.^name
tells us the type of $o
; in this case Programmer
.
$o.raku
returns a string that can be executed as Raku code, and
reproduces the original object $o
. While this does not work perfectly in
all cases, it is very useful for debugging simple objects.
[1]
The syntax of calling a method with .^
instead of a single dot means that
it is actually a method call on its metaclass, which is a class managing
the properties of the Programmer
class – or any other class you are
interested in. This metaclass enables other ways of introspection too:
say $o.^attributes.join(', ');
say $o.^parents.map({ $_.^name }).join(', ');
Finally $o.^name
calls the name
method on the metaobject, which
unsurprisingly returns the class name.
Given an object $mp3
and the MP3TagData
class definition from the section
Private methods, we can inquire about its
public methods with .^methods
:
my $mp3 = MP3TagData.new(filename => 'football-head.mp3');
say $mp3.^methods(:local);
# OUTPUT: (TWEAK filename title artist album year comment genre track version
# type Submethod+{is-hidden-from-backtrace}.new)
$mp3.^methods(:local)
produces a list of Methods that can be
called on $mp3
. The :local
named argument limits the returned methods to
those defined in the MP3TagData
class and excludes the inherited methods;
MP3TagData
inherits from no class, so providing :local
makes no difference.
To check if a type object (or an instance object) implements a certain public method, use the .^find-method metamethod, which returns the method object if it exists. Otherwise, it returns Mu.
say $mp3.^find_method('name'); # OUTPUT: «(Mu)»
say $mp3.^find_method('artist'); # OUTPUT: «artist»
Type objects can also be introspected for its private methods. However, public and private methods don't use the same APIs, and thus different metamethods must be used: .^private_methods and .^find_private_method.
say $mp3.^private_methods; # OUTPUT: «(parse-data can-read-format trim-nulls)»
say $mp3.^find_private_method('parse-data'); # OUTPUT: «parse-data»
say $mp3.^find_private_method('remove-nulls'); # OUTPUT: «(Mu)»
Introspection is very useful for debugging and for learning the language
and new libraries. When a function or method returns an object you don't
know about, by finding its type with .^name
, seeing a construction recipe
for it with .raku
, and so on, you'll get a good idea of what its return
value is. With .^methods
, you can learn what you can do with the class.
But there are other applications too. For instance, a routine that serializes objects to a bunch of bytes needs to know the attributes of that object, which it can find out via introspection.
Overriding default gist method
Some classes might need their own version of gist
, which overrides the terse
way they are printed when called to provide a default representation of the class.
For instance, exceptions might want
to write just the payload
and not the full object so that it is clearer what
to see what's happened. However, this isn't limited to exceptions; you can
do that with every class:
class Cook {
has @.utensils is rw;
has @.cookbooks is rw;
method cook( $food ) {
return "Cooking $food";
}
method clean_utensils {
return "Cleaning $_" for @.utensils;
}
multi method gist(Cook:U:) { '⚗' ~ self.^name ~ '⚗' }
multi method gist(Cook:D:) {
'⚗ Cooks with ' ~ @.utensils.join( " ‣ ") ~ ' using '
~ @.cookbooks.map( "«" ~ * ~ "»").join( " and ") }
}
my $cook = Cook.new(
utensils => <spoon ladle knife pan>,
cookbooks => ['Cooking for geeks','The French Chef Cookbook']);
say Cook.gist; # OUTPUT: «⚗Cook⚗»
say $cook.gist; # OUTPUT: «⚗ Cooks with spoon ‣ ladle ‣ knife ‣ pan using «Cooking for geeks» and «The French Chef Cookbook»»
Usually you will want to define two methods, one for the class and another for class instances; in this case, the class method uses the alembic symbol, and the instance method, defined below it, aggregates the data we have on the cook to show it in a narrative way.
A practical introspection example
When one creates a new class, it is sometimes useful to have informative (and safe) introspection accessible more easily as a public method. For example, the following class is used to hold attributes for a record row in a CSV spreadsheet with a header row defining its field (attribute) names.
unit class CSV-Record;
#| Field names and values for a CSV row
has $last;
has $first;
#...more fields (attributes)...
method fields(--> List) {
#| Return a list of the the attribute names (fields)
#| of the class instance
my @attributes = self.^attributes;
my @names;
for @attributes -> $a {
my $name = $a.name;
# The name is prefixed by its sigil and twigil
# which we don't want
$name ~~ s/\S\S//;
@names.push: $name;
}
@names
}
method values(--> List) {
#| Return a list of the values for the attributes
#| of the class instance
my @attributes = self.^attributes;
my @values;
for @attributes -> $a {
# Syntax is not obvious
my $value = $a.get_value: self;
@values.push: $value;
}
@values
}
We use it with a simple CSV file with contents:
last, first #...more fields...
Wall, Larry
Conway, Damian
Load the first record and show its contents:
my $record = CSV-Record.new: :$last, :$first;
say $record.fields.raku; # OUTPUT: «["last", "first"]»
say $record.values.raku; # OUTPUT: «["Wall", "Larry"]»
Note that practically we would have designed the class so that it has
the fields
list as a constant since its values are the same for all
class objects:
constant @fields = <last first>;
method fields(--> List) {
@fields
}
Downsides of using the introspective method for attribute names include slightly more processing time and power and the probable need to remove the sigil and twigil for public presentation.
[1]For example, closures cannot easily be reproduced this way; if you
don't know what a closure is don't worry. Also current implementations have
problems with dumping cyclic data structures this way, but they are expected
to be handled correctly by .raku
at some point.