💡 101. Quick sort in Perl 6

Today, let’s look at another, and presumably the most well known method of sorting data, Quick sort.

The algorithm requires you to select the so-called pivot, one of the element from the data, and split the rest in two parts: the elements less than the pivot, and the elements more or equals to the pivot. Each part is then recursively sorted again. On each iteration, the parts become smaller and smaller until the sublists are trivial data sets of one or even zero elements.

A separate theory is how to choose the right pivot. There are a few methods, for example, taking a value from the middle of the list, or taking the first item, or the last, or a random item. There are also more complicated methods, and you can tune it to achieve the best performance on your type of data sets.

For simplicity, let’s choose the first element as a pivot, and here is the code:

sub quick-sort(@data) {    
    return @data if @data.elems <= 1;

    my $pivot = @data[0];

    my @left;
    my @right;

    for @data[1..*] -> $x {
        if $x < $pivot {
            push @left, $x;
        }
        else {
            push @right, $x;
        }
    }

    return flat(quick-sort(@left), $pivot, quick-sort(@right));
}

my @data = 4, 5, 7, 1, 46, 78, 2, 2, 1, 9, 10;
@data = quick-sort @data;
say @data;

Unlike the previous examples of Bubble sort, this program does not sort in-place but returns a new list instead.

Now it is time to transform the code to make it more Perl 6-ish.

Multi-subs come for the rescue again, which while increasing the number of lines of code, make the whole algorithm easier to catch at a first glance.

multi sub quick-sort(@data where @data.elems <= 1) {
    return @data;
}

multi sub quick-sort(@data where @data.elems > 1) {
    my $pivot = @data[0];

    . . .
}

Now, take a look at these two pieces:

my $pivot = @data[0];

. . .

for @data[1..*] -> $x {

The first element needs to be taken, and the rest of the algorithm only deals with the rest of the list. Currently, this is achieved by taking an element and a slice, but there’s a shift method that does exactly what we need, and removes the element from the data. So, let’s use it:

my $pivot = @data.shift;

. . .

for @data -> $x {

The next comes the ifelse selection, which can be effectively (although maybe a bit less efficiently) be replaced with the two greps: one selecting the part prior to the pivot, another selecting the rest.

my @left = @data.grep(* < $pivot);
my @right = @data.grep(* >= $pivot);

Basically, that’s it. What you can also do is to get rid of temporary variables and put the filters to the return statement:

return flat(
    quick-sort(@data.grep(* < $pivot)),
    $pivot,
    quick-sort(@data.grep(* >= $pivot))
);

It all worked before this last change, but now it is broken:

$ perl6 quick-sort.pl 
Cannot call 'pop' on an immutable 'List'
   in sub quick-sort at 3.pl line 6
   in sub quick-sort at 3.pl line 8
   in block <unit> at 3.pl line 12

The problem is that you need to return a single list of numbers, but each subcall of quick-sort returns its own lists.

You can easily address the issue by slurping the elements by putting a * before the function argument:

multi sub quick-sort(*@data where @data.elems > 1) {
    . . .

The final code looks like this:

multi sub quick-sort(*@data where @data.elems <= 1) {
    return @data;
}

multi sub quick-sort(*@data where @data.elems > 1) {
    my $pivot = @data.shift;

    return flat(
        quick-sort(@data.grep(* < $pivot)),
        $pivot,
        quick-sort(@data.grep(* >= $pivot))
    );
}

Instead of using the flat routine, you can also flat each list independently using the |:

return
    |quick-sort(@data.grep(* < $pivot)),
    $pivot,
    |quick-sort(@data.grep(* >= $pivot));

But I think it is still better to have a couple of understandable intermediate variables to avoid punctuation noise:

multi sub quick-sort(@data where @data.elems <= 1) {
    return @data;
}

multi sub quick-sort(@data where @data.elems > 1) {
    my $pivot = @data.shift;

    my @left = @data.grep(* < $pivot);
    my @right = @data.grep(* >= $pivot);

    return flat(quick-sort(@left), $pivot, quick-sort(@right));
}

my @data = 4, 5, 7, 1, 46, 78, 2, 2, 1, 9, 10;
@data = quick-sort @data;
say @data;

As a home work, create an implementation of the Quick sort algorithm that sorts the data in-place.

The code is available on GitHub, you are welcome to join this journey too!

And let me update the post with based on some comments on Reddit.

First, instead of two greps, it was nice to use some function that does the separation in one go (as it was in the original program with if and else). Actually, Perl 6 has one. It is called classify, and it returns a hash, where the keys are all the possible values of the condition. In our case, it will be True (when the element is less than the pivot), and False otherwise.

multi sub quick-sort(@data where @data.elems > 1) {
    my $pivot = @data.shift;

    my %part = @data.classify(* < $pivot);

    return flat(
        quick-sort(%part{True} // []), 
        $pivot,
        quick-sort(%part{False} // [])
    );
}

As it may happen that all elements are on the same side of the pivot, you need to make sure you pass an empty list in that case (thus, // []).

The second change will lead to the the code similar to what is published (as of today) on Rosettacode. Indeed, if our choice of the pivot is the first element, than it is possible to separate it in the function signature instead of manually taking the item in the code. You can also simplify the signature of the first multi-method:

multi sub quick-sort([]) {
    return [];
}

multi sub quick-sort([$pivot, *@data]) {
    my %part = @data.classify(* < $pivot);

    return flat(
        quick-sort(%part{True} // []),
        $pivot,
        quick-sort(%part{False} // [])
    );
}

Rosettacode’s code also does not have a return statement. You can do that too, while I prefer having an explicit return in the function.

Can you suggest any further clever change?

💡 100. Bubble sort in Perl 6

Hey everyone, let’s implement some algorithms in Perl 6.

The first one will be the classical Bubble sort. In essence, you scan the data from left to right and swap the two adjacent items if they are not ordered properly yet. Repeat until the whole data list is ordered.

Here’s the initial straightforward implementation:

sub bubble-sort(@data) {
    my Bool $done = False;
    while !$done {
        $done = True;
        for 1 .. @data.elems - 1 -> $n {
            if @data[$n - 1] > @data[$n] {
                (@data[$n - 1], @data[$n]) = 
                    (@data[$n], @data[$n - 1]);
                $done = False;
            }
        }
    }
}

my @data = 4, 5, 7, 1, 46, 78, 2, 2, 1, 9, 10;
bubble-sort @data;
say @data;

This implementation does in-place sorting, but you don’t need to declare the function argument as is rw. Actually, the compiler will tell you that that is redundant and will stop further compilation:

For parameter '@data', '@' sigil containers don't need 'is rw' to be writable
Can only use 'is rw' on a scalar ('$' sigil) parameter, not '@data'

Another place which I’d like to take a more precise look at is swapping the two array elements:

(@data[$n - 1], @data[$n]) = (@data[$n], @data[$n - 1]);

Now, as the program is ready and we can confirm it works correctly, let’s transform it to make it more expressive (although maybe a bit less efficient).

$ perl6 bubble-sort.pl6 
[1 1 2 2 4 5 7 9 10 46 78]

The first step would be to simplify the swapping code. On both sides of the equals sign, we are working with the same two elements, with indices $n and $n - 1. It is possible to call the reverse method on the list of two elements and assign it back to it:

(@data[$n - 1], @data[$n]).=reverse;

This can be simplified further by using an array slice:

@data[$n - 1, $n].=reverse;

As you can see, the parentheses are not needed any more. The only question, which you have to answer yourself, is whether to surround the .= operator with spaces or not. On one hand, this is a method call (thus, no spaces), on another, it is an assignment (thus, spaces).

OK, what’s next? Of course, the if statement can be written as a postfix condition, but there are two lines of code in the if block. At this point, I can make a decision to trade between the beauty of code and the efficiency, and get rid of the Boolean $done flag.

sub bubble-sort(@data) {
    for 1 .. @data.elems - 1 -> $n {
        if @data[$n - 1] > @data[$n] {
            @data[$n - 1, $n].=reverse;
        }
    }
    bubble-sort(@data) unless [<=] @data;
}

This step also allowed to remove the while block. At the end of the function, a recursive call is done (again, maybe less efficient, but who cares), and the condition to check if the array is already sorted is now explicit:

unless [<=] @data

After all that, it is now possible to append a postfix if:

@data[$n - 1, $n].=reverse if @data[$n - 1] > @data[$n]

And even the postfix for:

@data[$_ - 1, $_].=reverse
    if @data[$_ - 1] > @data[$_]
        for 1 .. @data.elems - 1;

Also, why not make the condition using both the slice and the reduction operator as we’ve already done in other parts of the function:

@data[$_ - 1, $_].=reverse
    if [>] @data[$_ - 1, $_]
        for 1 .. @data.elems - 1;

The next step is to reduce the range 1 .. @data.elems - 1, which is OK but contains a bit too many elements, which can be removed. The -1 part can be replaced with an open range:

for 1 ..^ @data.elems;

After which the elems call is not needed, and Perl 6 can do that for us:

for 1 ..^ @data;

And here’s the current state of the code:

sub bubble-sort(@data) {
    @data[$_ - 1, $_] .= reverse
        if [>] @data[$_ - 1, $_]
            for 1 ..^ @data;
        
    bubble-sort(@data) unless [<=] @data;
}

my @data = 4, 5, 7, 1, 46, 78, 2, 2, 1, 9, 10;
bubble-sort @data;
say @data;

Before we stop for now, here’s another modification that is possible. In general, recursion in Perl 6 can be implemented via multi-subs. Instead of branching inside the function, let the compiler do the job based on data:

multi sub bubble-sort(@data where [<=] @data) {}

multi sub bubble-sort(@data where ![<=] @data) {
    @data[$_ - 1, $_] .= reverse
        if [>] @data[$_ - 1, $_]
            for 1 ..^ @data;
    bubble-sort(@data);
}

If you don’t like the [<=] check (as it should scan the whole list), here’s a brute-force version with another loop layer:

sub bubble-sort(@data) {
    for ^@data {
        @data[$_ - 1, $_] .= reverse
            if [>] @data[$_ - 1, $_] for 1 ..^ @data;
    }
}

The two for loops can be joined using the cross operator:

sub bubble-sort(@data) {
    for ^@data X 1 ..^ @data {
        my $n = $_[1];
        @data[$n - 1, $n] .= reverse if [>] @data[$n - 1, $n];
    }
}

You are invited to work on this code further. I am really curious if and how this can be done even shorter (for example, how to re-use the slice).

The source codes of the above-shown programs are available on GitHub.