Perl 6 - Introduction to application programming
In this tutorial, I’ll be guiding you through creating a simple application in Perl 6. If you don’t have Perl 6 installed yet, get the Rakudo Star distribution for your OS. Alternatively, you can use the Docker image.
The application itself will be a simple dice-roller. You give it a number of
dice to roll, and the number of sides the die has. We’ll start off by creating
it as a console application, then work to make it a GUI as well with the
GTK::Simple
module.
Preparation
First, you’ll want to install the libgtk headers. How to get these depends on
your distro of choice. For Debian-based systems, which includes Ubuntu and
derivatives, this command would be the following apt
invocation:
$ apt install libgtk-3-dev
For other distros, please consult your documentation.
To ease up module/application building, I’ll use App::Assixt
. This module
eases up on common tasks required for building other modules or applications.
So we’ll start by installing this module through zef
.
$ zef install App::Assixt
$PATH
as well, which can be done using hash -r
on bash
, or rehash
for zsh
. For other shells, consult your manual.
Next up, we can use assixt
to create the new skeleton of our application,
with the new
subcommand. This will ask for some user input, which will be
recorded in the META6.json
, a json-formatted file to keep track of meta
information about the module. assixt
should take care of this file for you,
so you never need to actually deal with it.
$ assixt new
assixt input
Since the assixt new
command requires some input, I’ll walk through these
options and explain how these would affect your eventual application.
Name of the module
This is the name given to the module. This will be used for the directory name,
which by default in assixt
will be perl6-
prepended to a lower-case version
of the module name. If you ever wish to make a module that is to be shared in
the Perl 6 ecosystem, this should be unique across the entire ecosystem. If
you’re interested in some guidelines, the PAUSE
guidelines seem
to apply pretty well to Perl 6 as well.
For this application, we’ll use Local::App::Dicer
, but you can use whatever
name you’d prefer here.
Your name
Your name. This will be used as the author’s name in the META6.json
. It is
used to find out who made it, in order to report issues (or words of praise,
of course).
Your email address
Your email address. Like your name, it will be used in case someone has to contact you in regards off the module.
Perl 6 version
This defaults to c
right now, and you can just hit enter to accept it. In the
future, there will be a Perl 6.d available as well, in which case you can use
this to indicate you want to use the newer features introduced in 6.d. This is
not the case yet, so you just want to go with the default c
value here.
Module description
A short description of your module, preferably a single sentence. This is useful to people wondering what the module is for, and module managers can show to the user.
License key
This indicates the license under which your module is distributed. This
defaults to GPL-3.0
, which I strongly recommend to use. The de-facto
default seems to be Artistic-2.0
, which is also used for Perl 6 itself.
This identifier is based on the SPDZ license list. Anything not mentioned in this list is not acceptable. #TODO Clarify why
Writing your first test
With the creation of the directory structure and metadata being taken care of
by assixt
, we can now start on writing things. Tests are not mandatory, but
are a great tool for quickly checking if everything works. If you make larger
applications, it really helps not having to manually test anything. Another
benefit is that you can quickly see if your changes, or those of someone else,
break anything.
Creating the base template for tests, assixt
can help you out again: assixt touch
can create templates in the right location, so you don’t have to deal
with it. In this case we want to create a test, which we’ll call “basic”.
$ assixt touch test basic
This will create the file t/basic.t
in your module directory. Its contents
will look as follows:
#! /usr/bin/env perl6
use v6.c;
use Test;
ok True;
done-testing;
# vim: ft=perl6
The only test it has right now is ok True
, which will always pass testing. We
will change that line into something more usable for this application:
use Local::App::Dicer;
plan 2;
subtest "Legal rolls", {
plan 50;
for 1..50 {
ok 1 ≤ roll($_) ≤ $_, "Rolls between 1 and $_";
}
}
subtest "Illegal rolls", {
plan 3;
throws-like { roll(0) }, X::TypeCheck::Binding::Parameter, "Zero is not accepted";
throws-like { roll(-1) }, X::TypeCheck::Binding::Parameter, "Negative rolls are not accepted";
throws-like { roll(1.5) }, X::TypeCheck::Binding::Parameter, "Can't roll half sides";
}
Perl 6 allows mathematical characters to make your code more concise, as with
the ≤ in the above block. If you use vim, you can make use
of the vim-perl6 plugin, which has an
option to change the longer, ascii-based ops (in this case \<=
) into the
shorter unicode based ops (in this case ≤
). This specific feature requires
let g:perl6_unicode_abbrevs = 1
in your vimrc
to be enabled with
vim-perl6
.
If that’s not an option, you can use a compose key. If that is not viable either, you can also stick to using the ascii-based ops. Perl 6 supports both of them.
This will run 53 tests, split up in two subtests. Subtests are used to logically group your tests. In this case, the calls that are correct are in one subtest, the calls that should be rejected are in another.
The plan
keywords indicate how many tests should be run. This will help spot
errors in case your expectations were not matched. For more information on
testing, check out the Perl 6 docs on
testing.
We’re making use of two test routines, ok
and throws-like
. ok
is a
simple test: if the given statement is truthy, the test succeeds. The other
one, throws-like
, might require some more explanation. The first argument it
expects is a code block, hence the { }
. Inside this block, you can run any
code you want. In this case, we run code that we know shouldn’t work. The
second argument is the exception it should throw. The test succeeds if the
right exception is thrown. Both ok
and throws-like
accept a descriptive
string as optional last argument.
Running the tests
A test is useless if you can’t easily run it. For this, the prove
utility
exists. You can use assixt test
to run these tests properly as well, saving
you from having to manually type out the full prove
command with options.
$ assixt test
You might notice the tests are currently failing, which is correct. The
Local::App::Dicer
module doesn’t exist yet to test against. We’ll be working
on that next.
assixt test
is prove -e "perl6 -Ilib" t
. This will include the lib
directory into the PERL6PATH
to be
able to access the libraries we’ll be making. The t
argument specifies the
directory containing the tests.
Creating the library
Again, let’s start with a assixt
command to create the base template. This
time, instead of touch test
, we’ll use touch lib
.
$ assixt touch unit Local::App::Dicer
This will generate a template file at lib/Local/App/Dicer.pm6
which some
defaults set. The file will look like this.
#! /usr/bin/env false
use v6.c;
unit module Local::App::Dicer;
The first line is a shebang. It
informs the shell what to do when you try to run the file as an executable
program. In this case, it will run false
, which immediately exits with a
non-success code. This file needs to be run as a Perl 6 module file, and
running it as a standalone file is an error.
The use v6.c
line indicates what version of Perl 6 should be used, and is
taken from the META6.json
, which was generated with assixt new
. The last
line informs the name of this module, which is Local::App::Dicer
. Beneath
this, we can add subroutines, which can be exported. These can then be accessed
from other Perl 6 files that use
this module.
Creating the roll
subroutine
Since we want to be able to roll
a die, we’ll create a subroutine to do
exactly that. Let’s start with the signature, which tells the compiler the name
of the subroutine, which arguments it accepts, their types and what type the
subroutine will return.
sub roll($sides) is export
{
$sides
}
Let’s break this down.
sub
informs the compiler we’re going to create a subroutine.roll
is the name of the subroutine we’re going to create.$sides
defines an argument used by the subroutine.is export
tells the compiler that this subroutine is to be exported. This allows access to the subroutine to another program that imports this module through ause
.{ $sides }
is the subroutine body. In Perl 6, the last statement is also the return value in a code block, thus this returns the value of $sides. A closing;
is also not required for the last statement in a block.
If you run assixt test
now, you can see it only fails 1/2 subtests:
# TODO: Add output of failing tests
Something is going right, but not all of it yet. The 3 tests to check for illegal rolls are still failing, because there’s no constraints on the input of the subroutine.
Adding constraints
The first constraint we’ll add is to limit the value of $sides
to an Int:D
.
The first part of this constraint is common in many languages, the Int
part.
The :D
requires the argument to be defined. This forces an actual
existing instance of Int
, not a Nil
or undefined value.
sub roll(Int:D $sides) is export
Fractional input is no longer allowed, since an Int
is always a round number.
But an Int
is still allowed to be 0 or negative, which isn’t possible in a
dice roll. Nearly every language will make you solve these two cases in the
subroutine body. But in Perl 6, you can add another constraint in the signature
that checks for exactly that:
sub roll(Int:D $sides where $sides > 0) is export
The where
part specifies additional constraints, in this case $sides > 0
.
So now, only round numbers larger than 0 are allowed. If you run assixt test
again, you should see all tests passing, indicating that all illegal rolls are
now correctly disallowed.
Returning a random number
So now that we can be sure that the input is always correct, we can start on
making the output more random. In Perl 6, you can take a number and call
.rand
on it, to get a random number between 0 and the value of the number you
called it on. This in turn can be rounded up to get a number ranging from 1 to
the value of the number you called .rand
on. These two method calls can also
be changed to yield concise code:
sub roll(Int:D $sides where $sides > 0) is export
{
$sides.rand.ceiling
}
That’s all we need from the library itself. Now we can start on making a usable program out of it.
Adding a console interface
First off, a console interface. assixt
can touch
a starting point for an
executable script as well, using assixt touch bin
:
$ assixt touch bin dicer
This will create the file bin/dicer
in your repository, with the following
template:
#! /usr/bin/env perl6
use v6.c;
sub MAIN
{
…
}
The program will run the MAIN
sub by default. We want to slightly change this
MAIN
signature though, since we want to accept user input. And it just so
happens that you can specify the command line parameters in the MAIN
signature in Perl 6. This lets us add constraints to the parameters and give
them better names with next to no effort. We want to accept two numbers, one
for the number of dice, and one for the number of sides per die:
sub MAIN(Int:D $dice, Int:D $sides where { $dice > 0 && $sides > 0 })
Here we see the where
applying constraints again. If you try running this
program in its current state, you’ll have to run the following:
$ perl6 -Ilib bin/dicer
Usage:
bin/dicer <dice> <sides>
This will return a list of all possible ways to invoke the program. There’s one slight problem right now. The usage description does not inform the user that both arguments need to be larger than 0. We’ll take care of that in a moment. First we’ll make this part work the way we want.
To do that, let’s add a use
statement to our lib
directory, and call the
roll
function we created earlier. The bin/dicer
file will come to look as
follows:
#! /usr/bin/env perl6
use v6.c;
use Local::App::Dicer;
sub MAIN(Int:D $dice, Int:D $sides where { $dice > 0 && $sides > 0 })
{
say $dice × roll($sides)
}
≤
character, Perl 6 allows to use the proper multiplication
character ×
(this is not the letter x
!). You can use the more widely known
*
for multiplication as well.
If you run the program with the arguments 2
and 20
now, you’ll get a random
number between 2 and 40, just like we expect:
$ perl6 -Ilib bin/dicer 2 20
18
The usage output
Now, we still have the trouble of illegal number input not clearly telling
what’s wrong. We can do a neat trick with the USAGE
sub to achieve
this. Perl 6 allows a subroutine with the name USAGE
to be defined, overriding
the default behaviour.
Using this, we can generate a friendlier message informing the user what they
need to supply more clearly. The USAGE
sub would look like this:
sub USAGE
{
say "Dicer requires two positive, round numbers as arguments."
}
If you run the program with incorrect parameters now, it will show the text
from the USAGE
subroutine. If the parameters are correct, it will run the
MAIN
subroutine.
You now have a working console application in Perl 6!
a simple GUI
But that’s not all. Perl 6 has a module to create GUIs with the
GTK library as well. For this, we’ll use the
GTK::Simple
module.
You can add this module as a dependency to the Local::App::Dicer
repository
with assixt
as well, using the depend
command. By default, this will also
install the dependency locally so you can use it immediately.
$ assixt depend GTK::Simple
Multi subs
Next, we could create another executable file and call it dicer-gtk
. However,
I can also use this moment to introduce
multi-methods.
These are subs with the same name, but differing signatures. If a call to such a
sub could potentially match multiple signatures, the most specific one will be
used. We will add another MAIN
sub, which will be called when bin/dicer
is
called with the --gtk
parameter.
We should also update the USAGE
sub accordingly, of course. And while we’re
at it, let’s also include the GTK::Simple
and GTK::Simple::App
modules. The
first pulls in all the different GTK elements we will use later on, while the
latter pulls in the class for the base GTK application window. The updated
MAIN
, USAGE
and use
parts will now look like this:
use Local::App::Dicer;
use GTK::Simple;
use GTK::Simple::App;
multi sub MAIN(Int:D $dice, Int:D $sides where { $dice > 0 && $sides > 0 })
{
say $dice × roll($sides)
}
multi sub MAIN(Bool:D :$gtk where $gtk == True)
{
# TODO: Create the GTK version
}
sub USAGE
{
say "Launch Dicer as a GUI with --gtk, or supply two positive, round numbers as arguments.";
}
There’s a new thing in a signature header here as well, :$gtk
. The :
in
front of it makes it a named argument, instead of a positional one. When used
in a MAIN
, this will allow it to be used like a long-opt, thus as --gtk
.
Its use in general subroutine signatures is explained in the next chapter.
Running the application with --gtk
gives no output now, because the body only
contains a comment. Let’s fix that.
Creating the window
First off, we require a GTK::Simple::App
instance. This is the main window,
in which we’ll be able to put elements such as buttons, labels, and input
fields. We can create the GTK::Simple::App
as follows:
my GTK::Simple::App $app .= new(title => "Dicer");
This one line brings in some new Perl 6 syntax, namely the .=
operator.
There’s also the use of a named argument in a regular subroutine.
The .=
operator performs a method on the variable on the left. In our case,
it will call the new
subroutine, which creates a new instance of the
GTK::Simple::App
class. This is commonly referred to as the constructor.
The named argument list (title => "Dicer"
) is another commonly used feature
in Perl 6. Any method can be given a non-positional, named parameter. This is
done by appending a :
in front of the variable name in the sub signature.
This has already been used in our code, in multi sub MAIN(Bool :$gtk where $gtk == True)
. This has a couple of benefits, which are explained in the
Perl 6 docs on signatures.
Creating the elements
Next up, we can create the elements we’d like to have visible in our application window. We needed two inputs for the console version, so we’ll probably need two for the GUI version as well. Since we have two inputs, we want labels for them. The roll itself will be performed on a button press. Lastly, we will want another label to display the outcome. This brings us to 6 elements in total:
- 3 labels
- 2 entries
- 1 button
my GTK::Simple::Label $label-dice .= new(text => "Amount of dice");
my GTK::Simple::Label $label-sides .= new(text => "Dice value");
my GTK::Simple::Label $label-result .= new(text => "");
my GTK::Simple::Entry $entry-dice .= new(text => 0);
my GTK::Simple::Entry $entry-sides .= new(text => 0);
my GTK::Simple::Button $button-roll .= new(label => "Roll!");
This creates all elements we want to show to the user.
Show the elements in the application window
Now that we have our elements, let’s put them into the application window.
We’ll need to put them into a layout as well. For this, we’ll use a grid. The
GTK::Simple::Grid
constructor takes pairs, with the key being a tuple
containing 4 elements, and the value containing the element you want to show.
The tuple’s elements are the x
, y
, w
and h
, which are the x
coordinates, y coordinates, width and height respectively.
This in turn takes us to the following statement:
$app.set-content(
GTK::Simple::Grid.new(
[0, 0, 1, 1] => $label-dice,
[1, 0, 1, 1] => $entry-dice,
[0, 1, 1, 1] => $label-sides,
[1, 1, 1, 1] => $entry-sides,
[0, 2, 2, 1] => $button-roll,
[0, 3, 2, 1] => $label-result,
)
);
Put a $app.run
beneath that, and try running perl6 -Ilib bin/dicer --gtk
.
That should provide you with a GTK window with all the elements visible in the
position we want. To make it a little more appealing, we can add a
border-width
to the $app
, which adds a margin between the border of the
application window, and the grid inside the window.
$app.border-width = 20;
$app.run;
You may notice that there’s no ()
after the run
method call. In Perl 6,
these are optional if you’re not supplying any arguments any way.
Binding an action to the button
Now that we have a visible window, it’s time to make the button perform an action. The action we want to execute is to take the values from the two inputs, roll the correct number of dice with the correct number of sides, and present it to the user.
The base code for binding an action to a button is to call .clicked.tap
on it,
and provide it with a code block. This code will be executed whenever the
button is clicked.
$button-roll.clicked.tap: {
};
You see we can also invoke a method using :
, and then supplying its
arguments. This saves you the trouble of having to add additional ( )
around
the call, and in this case it would be annoying to have to deal with yet
another set of parens.
Next, we give the code block something to actually perform:
$button-roll.clicked.tap: {
CATCH {
$label-result.text = "Can't roll with those numbers";
}
X::TypeCheck::Binding::Parameter.new.throw if $entry-dice.text.Int < 1;
$label-result.text = ($entry-dice.text.Int × roll($entry-sides.text.Int)).Str;
};
There’s some new things in this block of code, so let’s go over these.
CATCH
is the block in which we’ll end up if an exception is thrown in this scope.roll
will throw an exception if the parameters are wrong, and this allows us to cleanly deal with that.X::TypeCheck::Binding::Parameter.new.throw
throws a new exception of typeX::TypeCheck::Binding::Parameter
. This is the same exception type as thrown byroll
if something is wrong. We need to check the number of dice manually here, sinceroll
doesn’t take care of it, nor does any signature impose any restrictions on the value of the entry box.if
behind another statement. This is something Perl 6 allows, and in some circumstances can result in cleaner code. It’s used here because it improves the readability of the code, and to show that it’s possible.
The completed product
And with that, you should have a dice roller in Perl 6, with both a console and GTK interface. Below you can find the complete, finished sourcefiles which you should have by now.
t/basic.t
#! /usr/bin/env perl6
use v6.c;
use Test;
use Local::App::Dicer;
plan 2;
subtest "Legal rolls", {
plan 50;
for 1..50 {
ok 1 ≤ roll($_) ≤ $_, "Rolls between 1 and $_";
}
}
subtest "Illegal rolls", {
plan 3;
throws-like { roll(0) }, X::TypeCheck::Binding::Parameter, "Zero is not accepted";
throws-like { roll(-1) }, X::TypeCheck::Binding::Parameter, "Negative rolls are not accepted";
throws-like { roll(1.5) }, X::TypeCheck::Binding::Parameter, "Can't roll half sides";
}
done-testing;
# vim: ft=perl6
lib/Local/App/Dicer.pm6
#! /usr/bin/env false
use v6.c;
unit module Local::App::Dicer;
sub roll(Int:D $sides where $sides > 0) is export
{
$sides.rand.ceiling;
}
bin/dicer
#! /usr/bin/env perl6
use v6.c;
use Local::App::Dicer;
use GTK::Simple;
use GTK::Simple::App;
multi sub MAIN(Int:D $dice, Int:D $sides where { $dice > 0 && $sides > 0 })
{
say $dice × roll($sides)
}
multi sub MAIN(Bool:D :$gtk where $gtk == True)
{
my GTK::Simple::App $app .= new(title => "Dicer");
my GTK::Simple::Label $label-dice .= new(text => "Number of dice");
my GTK::Simple::Label $label-sides .= new(text => "Number of sides per die");
my GTK::Simple::Label $label-result .= new(text => "");
my GTK::Simple::Entry $entry-dice .= new(text => 0);
my GTK::Simple::Entry $entry-sides .= new(text => 0);
my GTK::Simple::Button $button-roll .= new(label => "Roll!");
$app.set-content(
GTK::Simple::Grid.new(
[0, 0, 1, 1] => $label-dice,
[1, 0, 1, 1] => $entry-dice,
[0, 1, 1, 1] => $label-sides,
[1, 1, 1, 1] => $entry-sides,
[0, 2, 2, 1] => $button-roll,
[0, 3, 2, 1] => $label-result,
)
);
$button-roll.clicked.tap: {
CATCH {
$label-result.text = "Can't roll with those numbers";
}
X::TypeCheck::Binding::Parameter.new.throw if $entry-dice.text.Int < 1;
$label-result.text = ($entry-dice.text.Int × roll($entry-sides.text.Int)).Str;
};
$app.border-width = 20;
$app.run;
}
sub USAGE
{
say "Launch Dicer as a GUI with --gtk, or supply two positive, round numbers as arguments.";
}
Installing your module
Now that you have a finished application, you probably want to install it as
well, so you can run it by calling dicer
in your shell. For this, we’ll be
using zef
.
To install a local module, tell zef
to try and install the local directory
you’re in:
$ zef install .
This will resolve the dependencies of the local module, and then install it.
You should now be able to run dicer
from anywhere.
$PATH
as well. On bash
, this is
done with hash -r
, on zsh
it’s rehash
. If you’re using any other shell,
please consult the manual.