🔬45. Exploring the pick and the roll methods in Perl 6, part 2

🔬45. Exploring the pick and the roll methods in Raku, part 2

N. B. Perl 6 has been renamed to Raku. Click to read more.


Today, we continue examining the internals of the pick and roll methods. Yesterday, we discovered how they work without arguments. It is time to see what the methods do to return multiple values.

We start with roll, as it is simpler as it does not care about returning unique elements. The roll($N) is a separate multi-sub with quite a few lines of code:

multi method roll(\number) {
    number == Inf
        ?? self.roll(*)
        !! self.is-lazy
        ?? X::Cannot::Lazy.new(:action('.roll from')).throw
        !! self.elems   # this allocates/reifies
            ?? Seq.new(class :: does Iterator {
                    has $!list;
                    has Int $!elems;
                    has int $!todo;
                    method !SET-SELF(\list,\todo) {
                        $!list := nqp::getattr(list,List,'$!reified');
                        $!elems = nqp::elems($!list);
                        $!todo  = todo;
                        self
                    }
                    method new(\list,\todo) {
                        nqp::create(self)!SET-SELF(list,todo)
                    }
                    method pull-one() is raw {
                        if $!todo {
                            $!todo = $!todo - 1;
                            nqp::atpos($!list,$!elems.rand.floor)
                        }
                        else {
                            IterationEnd
                        }
                    }
                }.new(self,number.Int))
            !! Seq.new(Rakudo::Iterator.Empty)
}

Let us try to understand what is happening here. First, the method delegates its work if the requested number of elements is infinite (we will discuss this case later) and rejects lazy lists with an exception:

> (1...Inf).roll(10);
Cannot .roll from a lazy list
  in block <unit> at <unknown file> line 1

The next check is a test whether the list has no elements: self.elems ??. If there are none, an empty sequence is returned:

!! Seq.new(Rakudo::Iterator.Empty)

By the way, this result differs from what a roll with no arguments would return for an empty list:

> say [].roll;
Nil

> say [].roll(1);
()

OK, we cut off all the edge cases and look at the central part of the routine, which is not that simple. Let us dig it step by step. First, a new sequence is prepared — it will be returned to the calling code.

Seq.new(

)

What do we see between the parentheses? A new object is created:

Seq.new(             
    {

    }.new(self,number.Int)
)

What kind of object? This object is an instance of the anonymous class that implements the Iterator role.

Seq.new(             
    class :: does Iterator {

    }.new(self,number.Int)
)

Finally, the class is filled with its attributes and methods. The role dictates the newly-built class to have some predefined methods, such as pull-one. The name suggests that the method returns one element each time it is called.

method pull-one() is raw {
    if $!todo {
        $!todo = $!todo - 1;
        nqp::atpos($!list,$!elems.rand.floor)
    }
    else {
        IterationEnd
    }
}

You may immediately understand how this works if I tell you that the $!todo attribute is an integer which is set to the value that was passed to the roll method. Thus, it works as a countdown counter. When zero is reached, IterationEnd is returned (which means to stop making more calls of pull-one).

You can trace the initialisation of the attributes in the anonymous class constructor as an exercise.

 

2 thoughts on “🔬45. Exploring the pick and the roll methods in Perl 6, part 2”

Leave a Reply

Your email address will not be published. Required fields are marked *

Retype the CAPTCHA code from the image
Change the CAPTCHA codeSpeak the CAPTCHA code