This is the third part of the "A gaze of iterators!" series.
By any other name
Let's look at the names of the iterators in part 2. For this, I'm going to use the .^name
method. As we've seen before, .^foo
means "calling the .foo
method on the object's meta-object".
say <a b c>.iterator.^name;
# Rakudo::Iterator::ReifiedListIterator
say (1..*).iterator.^name;
# Rakudo::Iterator::IntRangeUnending
say (1..10).pick(*).iterator.^name;
# List::PickN
say (1..10).map({++$_}).iterator.^name;
# Any::IterateOneWithoutPhasers
As you can see, the names of the classes of these iterator objects are all over the place. But they all provide the same interface: being able to call methods such as .pull-one
, .push-all
and .sink-all
.
In many programming languages, you'd expect all of these classes to be sharing the same parent class. In the Raku Programming Language you can indeed inherit from a parent class. You can check the lineage of a class with the .^mro
method (for method resolution order).
say <a b c>.iterator.^mro;
# ((ReifiedListIterator) (Any) (Mu))
say (1..*).iterator.^mro;
# ((IntRangeUnending) (Any) (Mu))
say (1..10).pick(*).iterator.^mro;
# ((PickN) (Any) (Mu))
say (1..10).map({++$_}).iterator.^mro;
# ((IterateOneWithoutPhasers) (Any) (Mu))
That is odd? They all seem to inherit from Any
and Mu
? Yet, one can not call the .pull-one
method on every object that just inherits from Any
and Mu
:
say 42.^mro;
# ((Int) (Cool) (Any) (Mu))
say 42.pull-one;
# No such method 'pull-one' for invocant of type 'Int'
Role playing
The Raku Programming Language also provides a thing called "roles". In short, you could think of a role as a collection of methods that will be "implanted" into a class if the class itself does not provide a method implementation for it.
All of these iterator classes that we've seen here, actually do the Iterator
role. And just as with the ^.mro
, you can introspect which roles a class performs by calling the .^roles
method. Let's see how that works out here:
say <a b c>.iterator.^roles;
# ((PredictiveIterator) (Iterator))
say (1..*).iterator.^roles;
# ((Iterator))
say (1..10).pick(*).iterator.^roles;
# ((Iterator))
say (1..10).map({++$_}).iterator.^roles;
# ((SlippyIterator) (Iterator))
So it looks like some classes are actually playing more than one role. But they all also do the Iterator
role, it looks like.
How to be an iterator
To make a class
be an iterator, one must tell the class to do the Iterator
role. That's pretty simple, no? Let's start with an empty class that just wants to be an iterator. You do that by using does
:
class Foo does Iterator {
}
===SORRY!=== Error while compiling -e
Method 'pull-one' must be implemented by Foo
because it is required by roles: Iterator.
So we need to actually provide some type of implementation for the interface that the Iterator
role is providing. Ok, so let's make a very simple method pull-one
that will randomly return True
or False
:
class TrueFalse does Iterator {
method pull-one() { Bool.roll }
}
say TrueFalse.pull-one; # True | False
The .roll
method randomly picks a single value from a set of values. When called on an enum
, it will randomly select one of the enums values. And the Bool
enum has True
and False
as its values.
Of course, this is all very boring, let's make it more interesting:
class YeahButNoBut does Iterator {
method pull-one() {
Bool.roll ?? "Yeah but" !! "No but"
}
}
say YeahButNoBut.pull-one; # Yeah but | No but
So we now have a class that produces an iterator. But how would you actually use that in any "normal" way in your program? Well, by embedding the iterator into another class, and have a method .iterator
in it that returns the iterator class:
class Jabbering {
method iterator() {
my class YeahButNoBut does Iterator {
method pull-one() {
Bool.roll ?? "Yeah but" !! "No but"
}
}
}
}
Note here that the .iterator
method actually returns the class object itself (usually referred to as the "type object"). Why? Because that's all we need from this iterator class: in its current form, this class doesn't need to keep any state.
Also note that classes in the Raku Programming Language can be lexically scoped by prefixing them with my
, just as you would lexically scoped variables. This makes sense in this case, as there would be no need for the iterator class outside of the scope of the "Jabbering" class.
So how would this look with by .^name
, ^.mro
and .^roles
, as we've shown with all of the other iterators?
say Jabbering.iterator.^name;
# Jabbering::YeahButNoBut
say Jabbering.iterator.^mro;
# ((YeahButNoBut) (Any) (Mu))
say Jabbering.iterator.^roles;
# ((Iterator))
As you can see, the Jabbering
class iterator has the expected name. And the Jabbering
class inherits from Any
and Mu
, and performs the Iterator
role.
So with all of this out of the way, now you can start jabbering!
.say for Jabbering;
Hmmm... that doesn't stop now, does it?
Indeed it doesn't. As to why, that's for the next instalment in this series!
Conclusion
This concludes the third part of the series, in which the concept of roles in the Raku Programming Language is introduced, along with does
. And that you can alter the scope of a class
by prefixing it with my
.
Questions and comments are always welcome. You can also drop into the #raku-beginner channel on Libera.chat, or on Discord if you'd like to have more immediate feedback.
I hope you liked it! Thank you for reading all the way to the end.