Perl 6 - Introduction to application programming

Tutorial Perl6 Assixt GTK Programming Raku — Published on .

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
note
You may need to rehash your $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";
}
note

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.

note
For those interested, the command run by 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.

tip
Perl 6 is gradually typed, so all type information is optional. The subroutine arguments are optional too, but you will rarely want a subroutine that doesn’t have an argument list.
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 a use.
  • { $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)
}
note
Just like the 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 type X::TypeCheck::Binding::Parameter. This is the same exception type as thrown by roll if something is wrong. We need to check the number of dice manually here, since roll 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.

warning
With most shells, you have to “rehash” your $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.