I’ve been trying to learn Perl 6 for the past few months. I’ve done it all. i.e. the usual stuff. Download Rakudo, install it, read the Perl 6 book. I did learn some stuff but I got no satisfaction! There is absolutely no substitute for hands on work. Get your hands dirty.
Rosetta Code is a great website where all kinds of programming tasks are solved using various languages. Its a great tool to learn new languages or improve your skills in your current language of choice. So I decided to pick up a task from this website. The task I first picked is the Balanced Brackets task.
Nope. Don’t even think about it. Look at the task definition on the website, copy it, paste it somewhere, and close that browser tab. You are not going to scroll down on that tab and look at the solutions. Here is the task definition.
Task definition
- Generate a string with N opening brackets (“
[”) and N closing brackets (“]”), in some arbitrary order.
- Determine whether the generated string is balanced; that is, whether it consists entirely of pairs of opening/closing brackets (in that order), none of which mis-nest.
Alright, lets approach this problem using TDD (test driven development). We will first create a series of tests all of which fail, and then gradually write the code so that all the tests pass.
In Perl 6, you can use the Test module. Its quite similar to the Perl 5 module. The only test we will be using is ok.
So here is how we write testing related code in Perl 6.
use Test;
ok (check_balance('[]') == True);
ok (check_balance('][') == False);
ok (check_balance('[[]]') == True);
ok (check_balance('][[]') == False);
ok (check_balance(']][[') == False);
ok (check_balance('[][]') == True);
ok (check_balance('[]][') == False);
ok (check_balance('[[[]]]') == True);
ok (check_balance('[][[[[]][]][]][]') == True);
ok (check_balance('[[]][][][[]][]') == True);
ok (check_balance('[[[][]][[]]]][[]') == False);
If you’re familiar with Perl 5 this should be easy enough to understand. We are checking the result of the check_balance subroutine (which we will write shortly). Now Perl 6 supports types. Strictly speaking, Perl 6 supports gradual typing. So it supports dynamic typing as well as static typing. The check_balance subroutine returns a value of type Bool which can have only two values – True or False. Note that True and False are not strings. The digit zero will evaluate to Bool::False but is of type Int.
Now lets write the basic stub of our check_balance subroutine.
sub check_balance(Str $b) returns Bool {
return False;
}
Perl 6 supports subroutine signatures. In this case we define the subroutine as accepting a single parameter of type Str (string) and returns a value of type Bool. Inside this subroutine, $b is the supplied argument. By default, the arguments are passed by copy.
Now lets get to the meat of this task. I must confess it took me several tries to get the logic correct, so I won’t bore you with the details. But as you develop your subroutine you will see the benefits of writing the tests first.
We need a way to keep track of how many brackets have been opened (the character [), and make sure that each opened bracket has been closed (the character ]). If at any moment we see that the number of closed brackets are more than the number of opened ones, we can be sure that the balance is off. At the same time we can’t do this the other way round, as we can start off with any number of opening brackets and then close them.
if $c eq '[' {
$num_opens++;
}
if $c eq ']' {
$num_opens--;
if $num_opens < 0 {
return False;
}
}
Here $c is each character in the string $b. So in effect, we need to apply the above code to each character of $b. Time for a loop! Lets approach this the easiest way. First, we need the number of characters in $b. That’s possible using $b.chars. So yes, in Perl 6 everything is an object and you can call methods on an object and you use the dot operator to do so. In Perl 6 the C-style for keyword is now loop.
loop (my $i = 0; $i < $b.chars - 1; $i++) {
my $c = $b.substr($i, 1);
if $c eq '[' {
$num_opens++;
}
if $c eq ']' {
$num_opens--;
if $num_opens < 0 {
return False;
}
}
}
From the above code snippet we can see that -
- No parentheses required for the if conditional
- You can call methods on an object using the dot operator
- The C-style for keyword is called loop
- String comparison operator is eq
At the end of this loop, we need to again check if the balance is correct. So $num_opens needs to be zero.
Here’s the complete subroutine.
sub check_balance(Str $b) returns Bool {
my $num_opens = 0;
loop (my $i = 0; $i < $b.chars - 1; $i++) {
my $c = $b.substr($i, 1);
if $c eq '[' {
$num_opens++;
}
if $c eq ']' {
$num_opens--;
if $num_opens < 0 {
return False;
}
}
}
return $num_opens == 0;
}
Now include this subroutine in your test script and all your tests should pass.
Lets dig deeper into Perl 6. After all, There’s more than one way to do it!
Calling $b.substr each time is a waste. Lets get all the characters of $b in an array and loop over that array.
sub check_balance(Str $b) returns Bool {
my $num_opens = 0;
my @chars = $b.split('');
for (0 .. @chars.elems - 1) -> $idx {
my $c = @chars[$idx];
if $c eq '[' {
$num_opens++;
}
if $c eq ']' {
$num_opens--;
if $num_opens < 0 {
return False;
}
}
}
return $num_opens == 0;
}
What have we learned ?
- Calling the split method on a string returns an array of characters
- We replace the loop with a for, and iterate over the elements of the array @chars
- @chars.elems return the size of @chars – an array
- The use of the .. sequence operator (as in Perl 5)
- We explicitly use the $idx variable as the topic variable instead of the default $_ inside the for loop
- The sigil for accessing individual array elements is now the same as the sigil of the array, and is not the $ (as was in Perl 5). Similarly you would access a hash value using, for example, %colors{‘red’}, and not $colors{‘red’}
The .. sequence operator supports exclusivity. So a caret (^) on a side does not include the last (or first) number. The for loop can thus be written as
for (0 ..^ @chars.elems) -> $idx {
...
}
Perl 6 has the comb method for strings which will iterate over the characters of a string. Lets look at the entire for loop.
for $b.comb -> $c {
if $c eq '[' {
$num_opens++;
}
if $c eq ']' {
$num_opens--;
if $num_opens < 0 {
return False;
}
}
}
Again note -
- The for loop does not require parentheses
You can check out all the code on Github. In the coming posts we will see how to generate the input bracket randomly, how to accept user input and finally how to create a Perl 6 module.
Have fun!