On This Page
Rockin' with Raku
Selected code examples showcasing the power of the Raku programming language. Watch it flex!
A Detailed Walk Through of Code for Finding Specific Prime Numbers
What's the 5th prime number? How about the 500th? 50,000th?! Easy (and super fast):
my $x = ^Inf;
sub get-prime(Int:D $nth where * > 0) {
say ($x.grep: *.is-prime)[$nth - 1];
}
for (5, 50, 500, 5000, 50000) { get-prime $_ };
When we run this, we get the following output after just a second or two:
11 # 5th prime
229 # 50th prime
3571 # 500th prime
48611 # 5000th prime
611953 # 50000th prime
Can you match the speed and conciseness of this code in your favorite language? More than likely, you'll have a hard time writing and executing this problem in a reasonable time. But with Raku, it's a snap.
Let's walk through the code in detail.
How it works
First we assign variable $x
to ^Inf
which represents a Range of Integers
from 0 to infinity. In Raku, a Range is a class which generates Range objects.
Range objects are "lazy," which means the actual set of numbers in the Range
aren't actually calculated or generated. This is unlike an Array where each
element is calculated ahead of time by the compiler and inserted into the array
and eats up memory. And so laziness makes your program much more efficient. You
certainly don't want to wait around for your computer to count to infinity but
you'd certainly run out of memory long before then anyway!
Next we create a simple subroutine called get-prime
that accepts a single
argument called $nth
, which is placed inside a set of parentheses. The
parentheses and the code inside of it is part the function's Signature, which
is itself an object. The Signature tells code calling get-prime
what kind of
parameters are acceptable to send. In get-prime
's signature, we ensure that
$nth
is a defined integer object by slapping Int:D
in front of it. The
Int
requires the caller to pass an Integer object. The :D
bit ensures that
it's a "defined" object, meaning that has a value associate with it. We also
add a nice touch by ensuring the number passed is greater than 0 with the
where * > 0
because it doesn't make sense to ask for the 0th or -1st prime
number. If you're you're wondering what the *
symbol is, hang tight. We'll
cover this symbol again shortly.
Restricting the kinds of argument that can be passed to a subroutine is known as "type checking." But thanks to Raku's flexibility, we could have chosen to not type check our arguments. If you want to bang out a simple script without a lot of hassle and headache trying to keep the compiler happy, Raku will accommodate your desires and not make onerous demands that make your work unnecessarily tedious.
Inside the subroutine is a single line of code consisting of a say
routine
that prints out the result of the expression to the right of it to standard
output, followed by a newline character. Every time get-prime
is called, a
new number, the prime number calculated by the expression, is printed out on a
separate line. Here it is:
say ($x.grep: *.is-prime)[$nth - 1];
So how is the number printed calculated? Well, the first thing you have
to understand is that the code inside the parentheses results in a data
structure called a Sequence which is basically a list of data in a specific
order. This means Sequences are a type of Positional data structure. For the
purposes of this discussion, that means we can find a specific value in the
Sequence using the square bracket notation, the [$nth - 1]
bit you see there
on the end where $nth
is the value passed into the subroutine. So this is
how we tell the code in the parentheses which prime number to pull out. Those
new to programming may be wondering why we subtract "1" from $nth
. This small
nice touch makes the subroutine more intuitive to use because a Sequence's
index number starts at '0', not '1'. So to get the first element, we pass in a
value of "1" which we gets reduced to "0" for us. If we didn't subtract one,
passing in a "5" would actually return the 6th prime number, which is
confusing.
You can imagine that in between the parentheses is every prime number that can exist from 0 to infinity (but that hasn't actually been calculated and so uses no memory). The number in the brackets chooses which number in that list to select, whether it be the 1st, 2nd, 3rd, 10th, or 10,232nd.
Now let's look inside the parentheses in our line of code in the subroutine,
the code that generates the Sequence. We can see our value $x
in there
representing every positive integer possible, both primary and non-primary
numbers. But, we don't want all numbers, we only want a list of all the primary
numbers. So what we do is run the grep
method on our infinite list of numbers
by placing a .
after $x
, followed by a method called grep
. grep
is
named after the command grep
found on computers since the 70s to find text
inside of lots of computer files on your storage device. You can think of
grep
as a filter that creates a list of stuff. It puts the stuff you want on
a list and keeps the stuff you don't want off of it. As you might guess, it's
the grep
method that generates our Sequence of prime numbers for us.
Now all that's left to do is to tell grep what numbers we want on our list. We
do that by passing an argument to grep
, expressed as *.is-prime
. Notice the
colon immediately after grep
. That indicates that what follows is the
argument we will be passing to the grep
method. Alternatively, we could do
away with the colon and surround the *.is-primate
argument with a set of
parentheses. In line with one of Raku's mantras, "There is more than one way to
do it" (TIMTOWTDI), it's up to you to decide what you think is most readable
in a given situation.
So what exactly is the *.is-prime
doing? It tells grep which stuff to place
into the Sequence. You can think of it as a test that runs on every number in
the range. First we look at the first number in the range, '0', and determine
if it's prime with the is-prime
method, one of the many math functions baked
into Raku, making it a great language for people interested in solving math
calculations. The is-prime
method returns True
if the number is prime and False
if
it isn't. If it's prime, grep will add the number to the Sequence. If it isn't
prime, grep will filter it out and it won't get added to the Sequence. Then we
go to the next number to determine if it's prime and so on until we reach
infinity.
But again we have to stress--and what makes this code so fast--is that we only do these calculations for the prime numbers we are interested in. It would be very wasteful to calculate the 1,000th prime number when we are only interested in the 5th.
You can you think of the *
symbol, what Raku calls the "Whatever" object, as
a placeholder for each number in our infinite list (kind of like how the "glob"
character is used in popular OS shells). The .is-prime
method gets run on the
"Whatever." Together, this expression can be read as "whatever is prime." The
expression returns True
if "Whatever" is prime or False
if it isn't. If
True
, grep puts the tested number into the Sequence, the list of all the
prime numbers, which again, isn't actually calculated (an impossible task).
Only the minimum work necessary is done to determine the specific prime number
we are looking for out of the theoretical series of all prime numbers that
exist.
After the get-prime
subroutine, we call it five different times, using a list of
5 numbers using a for loop
:
for 5, 50, 500, 5000, 50000 { get-prime $_ };
For each number in the list of numbers we supply, we run the code in the curly
braces which, in turn, calls the get-prime
subroutine for us. The technical
term for this kind of looping process is called "iteration".
Note the $_
variable in the curly braces. It has a special meaning in Raku.
It is called the "topic variable." It's helpful to think of it to mean the
"topic" of discussion. Each time we iterate over the list of our numbers, the
topic variable gets assigned to the next number in the list and this becomes
the $nth
parameter the get-prime
sub receives for its $nth
argument.
Wow, so that's well over 1,000 words to explain just four lines of Raku code. It demonstrates the power of Raku and how how efficient it can be. Amazingly, we can make this bit of code even more concise:
for (5, 50, 500, 5000, 50000) { ((^Inf).grep: *.is-prime)[$_ - 1].say };
And don't worry if this seems a little overwhelming. Most of Raku is much more straightforward than this example so don't let yourself get scared off. The larger takeaway is Raku excels at taking hard problems and making them a whole lot easier, even problems that are nearly impossible or impractical in many other programming languages.
The brevity of Raku's code also makes it much easier to understand what a large code base is doing because the code won't be cluttered with dozens of lines of code. And, of course, fewer lines of code means fewer bugs. And for the bugs that do crop up, they'll be easier for you to isolate and squash.