July 2016 Archives

IRC::Client: Perl 6 Multi-Server IRC (or Awesome Async Interfaces with Perl 6)

Read this article on Perl6.Party

I wrote my first Perl 6 program—a New Years IRC Party bot—around Christmas, 2015. The work included releasing the IRC::Client module, and given my virginity with the language and blood alcohol level appropriate for the Holiday Season, the module ended up sufficiently craptastic.

Recently, I needed a tool for some Perl 6 bug queue work, so I decided to lock myself up for a weekend and re-design and re-write the module from scratch. Multiple people bugged me to do so over the past months, so I figured I'd also write a tutorial for how to use the module—as an apology for being a master procrastinator. And should IRC be of no interest to you, I hope the tutorial will prove useful as a general example of async, non-blocking interfaces in Perl 6.

The Basics

To create an IRC bot, instantiate an IRC::Client object, giving it some basic info, and call the .run method. Implement all of the functionality you need as classes with method names matching the events you want to listen to and hand those in via the .plugins attribute. When an IRC event occurs, it's passed to all of the plugins, in the order you specify them, stopping if a plugin claims it handled the event.

Here's a simple IRC bot that responds to being addressed in-channel, notices, and private messages sent to it. The response is the uppercased original message the bot received:

use IRC::Client;
.run with IRC::Client.new:
    :nick<MahBot>
    :host<irc.freenode.net>
    :channels<#perl6>
    :debug
    :plugins(class { method irc-to-me ($_) { .text.uc } })

And here's what the bot looks like when running:

<Zoffix> MahBot, I ♥ you!
<MahBot> Zoffix, I ♥ YOU!

The :nick, :host, and :channels are the nick for your bot, the server it should connect to, and channels it should join. The :debug controls how much debugging output to display. We'll set it to value 1 here, for sparse debug output, just to see what's happening. Tip: install the optional Terminal::ANSIColor module to make debug output purty:

For the .plugins attribute, we hand in an anonymous class. If you have multiple plugins, just shove them all in in the order you want them to receive events in:

:plugins(PlugFirst.new, PlugSecond.new(:conf), class { ... })

The plugin class of our uppercasing bot has a single method that listens to irc-to-me event, triggered whenever the bot is addressed in-channel or is sent a private message or notice. It receives a single argument: one of the objects that does the IRC::Client::Message role. We stick it into the $_ topical variable to save a bit of typing.

We reply to the event by returning a value from the method. The original text is contained inside the .text attribute of the message object, so we'll call .uc method on it to uppercase the content and that's what our reply will be.

As awesome as our uppercasing bot is, it's as useful as an air conditioner on a polar expedition. Let's teach it some tricks.

Getting Smarter

We'll call our new plugin Trickster and it'll respond to commands time—that will give the local time and date—and temp—that will convert temperature between Fahrenheit and Celsius. Here's the code:

use IRC::Client;

class Trickster {
    method irc-to-me ($_) {
        given .text {
            when /time/ { DateTime.now }
            when /temp \s+ $<temp>=\d+ $<unit>=[F|C]/ {
                when $<unit> eq 'F' { "That's {($<temp> - 32) × .5556}°C" }
                default             { "That's { $<temp> × 1.8 + 32   }°F" }
            }
            'huh?'
        }
    }
}

.run with IRC::Client.new:
    :nick<MahBot>
    :host<irc.freenode.net>
    :channels<#perl6>
    :debug
    :plugins(Trickster)

<Zoffix> MahBot, time
<MahBot> Zoffix, 2016-07-23T19:00:15.795551-04:00
<Zoffix> MahBot, temp 42F
<MahBot> Zoffix, That's 5.556°C
<Zoffix> MahBot, temp 42C
<MahBot> Zoffix, That's 107.6°F
<Zoffix> MahBot, I ♥ you!
<MahBot> Zoffix, huh?

The code is trivial: we pass the given text over a couple of regexes. If it contains word time, we return the current time. If it contains word temp we do the appropriate math, based on whether the given number is postfixed by an F or a C. And if no matches happen, we end up returning the inquisitive huh?.

There's an obvious problem with this new and improved plugin: the bot no longer loves me! And while I'll survive the heartache, I doubt any other plugin will teach the bot to love again, as Trickster consumes all irc-to-me events, even if it doesn't recognize any of the commands it can handle. Let's fix that!

Passing The Buck

There's a special value that can be returned by the event handler to signal that it did not handle the event and that it should be propagated to further plugins and event handlers. That value is provided by the .NEXT attribute offered by the IRC::Client::Plugin role, which a plugin does to obtain that attribute. The role is automatically exported when you use IRC::Client.

Let's look at some code utilizing that special value. Note that since .NEXT is an attribute and we can't look up attributes on type objects, you need to go the extra step and instantiate your plugin classes when giving them to :plugins.

use IRC::Client;

class Trickster does IRC::Client::Plugin {
    method irc-to-me ($_) {
        given .text {
            when /time/ { DateTime.now }
            when /temp \s+ $<temp>=\d+ $<unit>=[F|C]/ {
                when $<unit> eq 'F' { "That's {($<temp> - 32) × .5556}°C" }
                default             { "That's { $<temp> × 1.8 + 32   }°F" }
            }
            $.NEXT;
        }
    }
}

class BFF does IRC::Client::Plugin {
    method irc-to-me ($_) {
        when .text ~~ /'♥'/ { 'I ♥ YOU!' };
        $.NEXT;
    }
}

.run with IRC::Client.new:
    :nick<MahBot>
    :host<irc.freenode.net>
    :channels<#perl6>
    :debug
    :plugins(Trickster.new, BFF.new)

<Zoffix> MahBot, time
<MahBot> Zoffix, 2016-07-23T19:37:45.788272-04:00
<Zoffix> MahBot, temp 42F
<MahBot> Zoffix, That's 5.556°C
<Zoffix> MahBot, temp 42C
<MahBot> Zoffix, That's 107.6°F
<Zoffix> MahBot, I ♥ you!
<MahBot> Zoffix, I ♥ YOU!

We now have two plugins that both subscribe to irc-to-me event. The :plugins attribute receives Trickster plugin first, so its event handler will be run first. If the received text does not match either of the Trickster's regexes, it returns $.NEXT from the method.

That signals the Client Object to go hunting for other handlers, so it gets to BFF's irc-to-me handler. There, we reply if the input contains a heart, if not, we pre-emptively return $.NEXT here too.

While the bot got its sunny disposition back, it did so at the cost of quite a bit of extra typing. What can we do about that?

Multify All The Things!

Perl 6 supports multi-dispatch as well as type constraints in signatures. On top of that, smartmatch against IRC::Client's message objects that have a .text attribute uses the value of that attribute. Combine all three of those features and you end up with ridiculously concise code:

use IRC::Client;
class Trickster {
    multi method irc-to-me ($ where /time/) { DateTime.now }
    multi method irc-to-me ($ where /temp \s+ $<temp>=\d+ $<unit>=[F|C]/) {
        $<unit> eq 'F' ?? "That's {($<temp> - 32) × .5556}°C"
                       !! "That's { $<temp> × 1.8 + 32   }°F"
    }
}

class BFF { method irc-to-me ($ where /'♥'/) { 'I ♥ YOU!' } }

.run with IRC::Client.new:
    :nick<MahBot>
    :host<irc.freenode.net>
    :channels<#perl6>
    :debug
    :plugins(Trickster, BFF)

<Zoffix> MahBot, time
<MahBot> Zoffix, 2016-07-23T19:59:44.481553-04:00
<Zoffix> MahBot, temp 42F
<MahBot> Zoffix, That's 5.556°C
<Zoffix> MahBot, temp 42C
<MahBot> Zoffix, That's 107.6°F
<Zoffix> MahBot, I ♥ you!
<MahBot> Zoffix, I ♥ YOU!

Outside of the signature, we no longer have any need for the message object, so we use the anonymous $ parameter in its place. We then type-constrain that parameter with a regex match, and so the method will be called only if the text of the message matches that regex. Since no methods will be called on failed matches, we no longer have to mess around with the whole $.NEXT business or compose any roles into our plugins.

The bodies of our methods each have a single statement that produces the response value for the event. In the temperature converter, we use the ternary operator to select which formula to use for the conversion, depending on the unit requested, and yes, the $<unit> and $<temp> captures created in the signature type constraint match are available in the method's body.

An Eventful Day

Along with standard named and numerical IRC protocol events, IRC::Client offers convenience events. One of them we've already seen: the irc-to-me event. Such events are layered, so one IRC event can trigger several IRC::Client's events. For example, if someone addresses our bot in a channel, the following chain of events will be fired:

irc-addressed  ▶  irc-to-me  ▶  irc-privmsg-channel  ▶  irc-privmsg  ▶  irc-all

The events are ordered from "narrowest" to "widest": irc-addressed can be triggered only in-channel, when our bot is addressed; irc-to-me can also be triggered via notice and private message, so it's wider; irc-privmsg-channel includes all channel messages, so it's wider still; and irc-privmsg also includes private messages to our bot. The chain ends by the widest event of them all: irc-all.

If a plugin's event handler returns any value other than $.NEXT, later events in the event chain won't be fired, just as plugins later in the plugin chain won't be tried for the same reason. Each event is tried on all of the plugins, before attempting to handle a wider event.

By setting the :debug attribute to level 3 or higher, you'll get emitted events in the debug output. Here's our bot attempting to handle unknown command blarg and then processing command time handled by irc-to-me event handler we defined:

All of IRC::Client's events have irc- prefix, so you can freely define auxiliary methods in your plugin, without worrying about conflicting with event handlers. Speaking of emitting things...

Keep 'Em Commin'

Responding to commands is sweet and all, but many bots will likely want to generate some output out of their own volition. As an example, let's write a bot that will annoy us whenever we have unread GitHub notifications!

use IRC::Client;
use HTTP::Tinyish;
use JSON::Fast;

class GitHub::Notifications does IRC::Client::Plugin {
    has Str  $.token  = %*ENV<GITHUB_TOKEN>;
    has      $!ua     = HTTP::Tinyish.new;
    constant $API_URL = 'https://api.github.com/notifications';

    method irc-connected ($) {
        start react {
            whenever self!notification.grep(* > 0) -> $num {
                $.irc.send: :where<Zoffix>
                            :text("You have $num unread notifications!")
                            :notice;
            }
        }
    }

    method !notification {
        supply {
            loop {
                my $res = $!ua.get: $API_URL, :headers{ :Authorization("token $!token") };
                $res<success> and emit +grep *.<unread>, |from-json $res<content>;
                sleep $res<headers><X-Poll-Interval> || 60;
            }
        }
    }
}

.run with IRC::Client.new:
    :nick<MahBot>
    :host<irc.freenode.net>
    :channels<#perl6>
    :debug
    :plugins(GitHub::Notifications.new)

[00:25:41] -MahBot- Zoffix, You have 20 unread notifications!
[00:26:41] -MahBot- Zoffix, You have 19 unread notifications!

We create GitHub::Notifications class that does the IRC::Client::Plugin role. That role gives us the $.irc attribute, which is the IRC::Client object we'll use to send messages to us on IRC.

Aside from irc-connected method, the class is just like any other: a public $.token attribute for our GitHub API token, a private $!ua attribute that keeps our HTTP User Agent object around, and a private notification method, where all the action happens.

Inside notification, we create a Supply that will emit the number of unread notifications we have. It does so by using an HTTP::Tinyish object to access a GitHub API endpoint. On line 24, it parses the JSON returned by successful requests, and greps the message list for any items with unread property set to true. The prefix + operator converts the list to an Int that is total items found, which is what we emit from our supply.

The irc-connected event handler gets triggered when we successfully connect to an IRC server. In it, we start an event loop that reacts whenever we receive the current unread messages count from our supply given by notifications method. Since we're only interested in cases where we do have unread messages, we also pop a grep on the supply to filter out the cases without any messages (yes, we could avoid emitting those in the first place, but I'm showing off Perl 6 here 😸). And once we do have unread messages, we simply call IRC::Client's .send method, asking it to send us an IRC notice with the total number of unread messages. Pure awesomeness!

Don't Wait Up

We've covered the cases where we either have an asynchronous supply of values we sent to IRC or where we reply to a command right away. It's not uncommon for a bot command to take some time to execute. In those cases, we don't want the bot to lock up while the command is doing its thing.

Thanks to Perl 6's excellent concurrency primitives, it doesn't have to! If an event handler returns a Promise, the Client Object will use its .result as the reply when it is kept. This means that in order to make our blocking event handler non-blocking, all we have to do is wrap its body in a start { ... } block. What could be simpler?

As an example, let's write a bot that will respond to bash command. The bot will fetch bash.org/?random1, parse out the quotes from the HTML, and keep them in the cache. When the command is triggered, the bot will hand out one of the quotes, repeating the fetching when the cache runs out. In particular, we don't want the bot to block while retrieving and parsing the web page. Here's the full code:

use IRC::Client;
use Mojo::UserAgent:from<Perl5>;

class Bash {
    constant $BASH_URL = 'http://bash.org/?random1';
    constant $cache    = Channel.new;
    has        $!ua    = Mojo::UserAgent.new;

    multi method irc-to-me ($ where /bash/) {
        start $cache.poll or do { self!fetch-quotes; $cache.poll };
    }

    method !fetch-quotes {
        $cache.send: $_
            for $!ua.get($BASH_URL).res.dom.find('.qt').each».all_text.lines.join: '  ';
    }
}

.run with IRC::Client.new:
    :nick<MahBot>
    :host<irc.freenode.net>
    :channels<#perl6>
    :debug
    :plugins(Bash.new)

<Zoffix> MahBot, bash
<MahBot> Zoffix, <Time> that reminds me of when Manning and I installed OS/2 Warp4 on a box and during the install routine it said something to the likes of 'join the hundreds of people on the internet'

For page fetching needs, I chose Perl 5's Mojo::UserAgent, since it has an HTML parser built-in. The :from<Perl5> adverb indicates to the compiler that we want to load a Perl 5, not Perl 6, module.

Since we're multi-threading, we'll use a Channel as a thread-safe queue for our caching purposes. We subscribe to the irc-to-me event where text contains word bash. When the event handler is triggered, we pop out to a new thread using the start keyword. Then we .poll our cache and use the cached value if we have one, otherwise, the logic will move onto the do block that that calls the fetch-quotes private method and when that completes, polls the cache once more, getting a fresh quote. All said and done, a quote will be the result of the Promise we return from the event handler.

The fetch-quotes method fires up our Mojo::UserAgent object that fetches the random quotes page from the website, finds all HTML elements that have class="qt" on them—those are paragraphs with quotes. Then, we use a hyper method call to convert those paragraphs to just text and that final list is fed to our $cache Channel via a for loop. And there you go, we non-blockingly connected our bot to the cesspit of the IRC world. And speaking of things you may want to filter...

Watch Your Mouth!

Our bot would get banned rather quickly if it spewed enormous amounts of output into channels. An obvious solution is to include logic in our plugins that would use a pastebin if the output is too large. However, it's pretty impractical to add such a thing to every plugin we write. Luckily, IRC::Client has support for filters!

For any method that issues a NOTICE or PRIVMSG IRC command, IRC::Client will pass the output through classes given to it via :filters attribute. This means we can set up a filter that will automatically pastebin large output, regardless of what plugin it comes from.

We'll re-use our bash.org quote bot, except this time it will pastebin large quotes to Shadowcat pastebin. Let's look at some code!

use IRC::Client;
use Pastebin::Shadowcat;
use Mojo::UserAgent:from<Perl5>;

class Bash {
    constant $BASH_URL = 'http://bash.org/?random1';
    constant $cache    = Channel.new;
    has        $!ua    = Mojo::UserAgent.new;

    multi method irc-to-me ($ where /bash/) {
        start $cache.poll or do { self!fetch-quotes; $cache.poll };
    }

    method !fetch-quotes {
        $cache.send: $_
            for $!ua.get($BASH_URL).res.dom.find('.qt').each».all_text;
    }
}

.run with IRC::Client.new:
    :nick<MahBot>
    :host<irc.freenode.net>
    :channels<#zofbot>
    :debug
    :plugins(Bash.new)
    :filters(
        -> $text where .lines > 1 || .chars > 300 {
            Pastebin::Shadowcat.new.paste: $text.lines.join: "\n";
        }
    )

<Zoffix> MahBot, bash
<MahBot> Zoffix, <intuit> hmm maybe sumtime next week i will go outside'
<Zoffix> MahBot, bash
<MahBot> Zoffix, http://fpaste.scsys.co.uk/528741

The code that does all the filtering work is small enough that it's easy to miss—it's the last 5 lines in the program above. The :filters attribute takes a list of Callables, and here we're passing a pointy block. In its signature we constraint the text to be more than 1 line or more than 300 characters long, so our filter will be run only when those criteria are met. Inside the block, we simply use the Pastebin::Shadowcat module to throw the output onto the pastebin. Its .paste method returns the URL of the newly-created paste, which is what our filter will replace the original content with. Pretty awesome!

It Spreads Like Butter

In the past, when I used other IRC client tools, whenever someone asked me to place my bots on other servers, the procedure was simple: copy over the code to another directory, change config, and you're done. It almost made sense that a new server would mean a "new" bot: different channels, different nicknames, and so on.

In Perl 6's IRC::Client, I tried to re-imagine things a bit: a server is merely another identifier for a message, along with a channel or nickname. This means connecting your bot to multiple servers is as simple as adding new server configuration via :servers attribute:

use IRC::Client;

class BFF {
    method irc-to-me ($ where /'♥'/) { 'I ♥ YOU!' }
}

.run with IRC::Client.new:
    :debug
    :plugins(BFF)
    :nick<MahBot>
    :channels<#zofbot>
    :servers(
        freenode => %(
            :host<irc.freenode.net>,
        ),
        local => %(
            :nick<P6Bot>,
            :channels<#zofbot #perl6>,
            :host<localhost>,
        )
    )

[on Freenode server]
<ZoffixW> MahBot, I ♥ you
<MahBot> ZoffixW, I ♥ YOU!

[on local server]
<ZoffixW> P6Bot, I ♥ you
<P6Bot> ZoffixW, I ♥ YOU!

First, our plugin remains oblivious that it's being run on multiple servers. Its replies get redirected to the correct server and IRC::Client still executes its method handler in a thread-safe way.

In the IRC::Client's constructor we added :servers attribute that takes a Hash. The keys of this Hash are servers' labels and values are server-specific configurations that override global settings. So freenode server gets its :nick and :channels from the :nick and :channels attributes we give to IRC::Client, while the local server overrides those with its own values.

The debug output now has server lables printed, to indicate to which server the event applies:

And so, but simply telling the bot to connect to another server, we made it multi-server, without making any changes to our plugins. But what do we do when we do want to talk to a specific server?

Send It That Way

When the bot is .run, the Client Object changes the values of :servers attribute to be IRC::Client::Server objects. Those stringify to the label for the server they represent and we can get them either from the .server attribute of the Message Object or .servers hash attribute of the Client Object. Client Object methods such as .send or .join take an optional server attribute that controls which server the message will be sent to and defaults to value *, which means send to every server.

Here's a bot that connects to two servers and joins several channels. Whenever it sees a channel message, it forwards it to all other channels and sends a private message to user Zoffix on server designated by label local.

use IRC::Client;

class Messenger does IRC::Client::Plugin {
    method irc-privmsg-channel ($e) {
        for $.irc.servers.values -> $server {
            for $server.channels -> $channel {
                next if $server eq $e.server and $channel eq $e.channel;

                $.irc.send: :$server, :where($channel), :text(
                    "$e.nick() over at $e.server.host()/$e.channel() says $e.text()"
                );
            }
        }

        $.irc.send: :where<Zoffix>
                    :text('I spread the messages!')
                    :server<local>;
    }
}

.run with IRC::Client.new:
    :debug
    :plugins[Messenger.new]
    :nick<MahBot>
    :channels<#zofbot>
    :servers{
        freenode => %(
            :host<irc.freenode.net>,
        ),
        local => %(
            :nick<P6Bot>,
            :channels<#zofbot #perl6>,
            :host<localhost>,
        )
    }

[on Freenode server/#zofbot]
<ZoffixW> Yey!
[on local server/#zofbot]
<P6Bot> ZoffixW over at irc.freenode.net/#zofbot says Yey!
[on local server/#perl6]
<P6Bot> ZoffixW over at irc.freenode.net/#zofbot says Yey!
[on local server/ZoffixW private message queue]
<P6Bot> I spread the messages!

We subscribe to the irc-privmsg-channel event and when it's triggered, we loop over all the servers. For each server, we loop over all of the connected channels and use $.irc.send method to send a message to that particular channel and server, unless the server and channel are the same as where the message originated.

The message itself calls .nick, .channel, and .server.host methods on the Message Object to identify the sender and origin of the message.

Conclusion

Perl 6 offers powerful concurrency primitives, dispatch methods, and introspection that lets you build awesome non-blocking, event-based interfaces. One of them is IRC::Client that lets you use IRC networks. It's here. It's ready. Use it!

Announce: Rakudo Star Perl 6 Compiler Distribution Release 2016.07

Re-posted from http://rakudo.org/2016/07/22/announce-rakudo-star-release-2016-07/
The release was prepared by Steve 'stmuk' Mynott

On behalf of the Rakudo and Perl 6 development teams, I'm pleased to announce the July 2016 release of "Rakudo Star", a useful and usable production distribution of Perl 6. The tarball for the July 2016 release is available from http://rakudo.org/downloads/star/.

This is the third post-Christmas (production) release of Rakudo Star and implements Perl v6.c. It comes with support for the MoarVM backend (all module tests pass on supported platforms).

Please note that this release of Rakudo Star is not fully functional with the JVM backend from the Rakudo compiler. Please use the MoarVM backend only.

In the Perl 6 world, we make a distinction between the language ("Perl 6") and specific implementations of the language such as "Rakudo Perl". This Star release includes release 2016.07 of the Rakudo Perl 6 compiler, version 2016.07 of MoarVM, plus various modules, documentation, and other resources collected from the Perl 6 community.

Some of the new compiler features since the last Rakudo Star release include:

  • Ability to use a custom debugger module
  • The "is-approx" sub from Test.pm6 now allows for relative/absolute tolerance
  • A fail in a custom BUILD will now be returned, rather than thrown
  • Introduce .Map coercer
  • Implement alternate ways to call subtest
  • Support for new leap-second at the end of 2016
  • The "is required" trait on Attributes can now take a Bool or a Str
  • IO::[Path,Handle] gained a .mode method which returns the POSIX file permissions
  • Distribution is now a role interface that enables encapsulating IO used for distribution installation
  • CompUnit::Repository::Installation now uses the new Distribution interface
  • Custom repository implementations now supported, including precompilation

Compiler maintenance since the last Rakudo Star release includes:

  • Basic object creation (using either .new or .bless) now up to 3x faster
  • All routines now have less overhead
  • The MMD cache accepts candidates with named parameters if it can. (This made adverbed slices about 18x as fast)
  • Sigificant optimizations for speed in many parts of the system (.map, gather/take etc.)
  • Many precompilation fixes (including EVAL and improved support of OS packaging)
  • Arrays with holes (e.g. from :delete) now correctly iterate/auto-vivify
  • An issue with reverse dependencies of installed modules was fixed
  • "is_approx" sub (note underscore) from Test.pm6 deprecated
  • Harden Mu.Str against moving GC
  • Simplify $USER/$GROUP initialization
  • Mu can now be the result of a Promise
  • samewith() now also works on non-multi's
  • Many fixes in the area of pre-compilation and installing modules
  • count-only and bool-only now are optional methods in Iterators (only to be implemented if they can work without generating anything)
  • IO::ArgFiles.slurp / IO::ArgFiles.eof are fixed
  • REPL whitespace and error handling
  • CompUnit::Repository::Installation no longer considers bin/xxx and resources/bin/xxx the same content address
  • min/max on Failures throw instead of returning ±Inf
  • NativeCall's is mangled trait no longer ignored for CPPStruct
  • Many Str, List and Array methods much faster
  • Map/Hash initializations are now 30% faster
  • make DESTDIR now correctly finds CompUnit::Repository::Staging
  • Output from Test.pm6's diag() is no longer lost in non-verbose prove output when called at the start of the test file or during TODO tests
  • Improved error messages

Notable changes in modules shipped with Rakudo Star:

  • DBIish: v0.5.9 (with many Oracle/MySQL fixes) plus README.pod and mojibake fixes
  • NativeHelpers-Blob: v0.1.10
  • PSGI: v1.2.0 supports P6SGI 0.7Draft
  • Pod-To-HTML: v0.1.2 plus fixes
  • debugger-ui-commandline: README fixes
  • doc: many fixes to documentation content and HTML generation
  • panda: Avoid Rakudo internals deprecation warning and don't require Build.pm to inherit Panda::Builder
  • perl6-file-which: CI fixes
  • perl6-http-easy: v1.1.0 (with more flexible P6SGI support) plus avoid errors in binary request
  • shell-command: Mention already implemented commands missing from README
  • perl6-lwp-simple: track github.com/perl6/perl6-lwp-simple as upstream (as panda does) which has a test fix needed since we don't support https in R* and a test url had a new https redirect

perl6intro.pdf has also been updated.

There are some key features of Perl 6 that Rakudo Star does not yet handle appropriately, although they will appear in upcoming releases. Some of the not-quite-there features include:

  • advanced macros
  • non-blocking I/O (in progress)
  • some bits of Synopsis 9 and 11
  • There is an online resource at http://perl6.org/compilers/features that lists the known implemented and missing features of Rakudo's backends and other Perl 6 implementations.

In many places we've tried to make Rakudo smart enough to inform the programmer that a given feature isn't implemented, but there are many that we've missed. Bug reports about missing and broken features are welcomed at rakudobug@perl.org.

See http://perl6.org/ for links to much more information about Perl 6, including documentation, example code, tutorials, presentations, reference materials, design documents, and other supporting resources. Some Perl 6 tutorials are available under the "docs" directory in the release tarball.

The development team thanks all of the contributors and sponsors for making Rakudo Star possible. If you would like to contribute, see http://rakudo.org/how-to-help, ask on the perl6-compiler@perl.org mailing list, or join us on IRC #perl6 on freenode.

[Part 2] A Date With The Bug Queue or Let Me Help You Help Me Help You

Read this article on Perl6.Party

Be sure to read Part I of this series.

As I was tagging tickets in my bug ticket helper app, I was surprised by how often I was tagging tickets with this particular tag:

Needs core member decision

It may have been the most used tag of them all. And so, it made be think about...

PART II: The Experienced Contributor

I will be referring to "core developers," but this generally applies to any person who has great familarity with the project, how it should, does, and will work —The Experienced Contributor. When it comes to bug queues, can this type of people do more than just pick the bug they like the most and fix it?

LESSON 3: Many Tickets Can Be Fixed With A Single Comment

On my date with the bug queue, I found many tickets that looked relatively easy to fix, from a technological point of view, but I couldn't even begin working on them for a simple reason: I didn't know what the correct behaviour should be.

Here's a good example:

> "345".Int
345
> "3z5".Int
Cannot convert string to number: trailing characters after number in '3^z5'
> "34\x[308]5".Int
3

When non-numeric characters are found, the conversion from Str to Int fails with an error message, but if a diacritic is present on a digit, the conversion stops and returns only a partial number. The issue sounds simple enough, so let's get cracking? But wait!

What is the correct behaviour here? Should it fail with the same error message as "3z5" fails with, since a diacritic isn't a digit, or should it be silently ignored? After all, the entire string does match \d+:

say "34\x[308]5" ~~ /^ \d+ $/
# OUTPUT:
# 「34̈5」

In this case, we have a contributor ready to resolve the ticket, but they are stimied by the lack of direction. When a core member sees a ticket like that, and they don't have the time to fix the bug, they should comment on the ticket, indicating what the expected behaviour is. Now, a different contributor who does have the time can proceed to fix the bug with great confidence their patch will be accepted.

And just like that, with a single comment from a core developer the ticket became much easier to fix! And speaking of easy things to fix...

LESSON 4: Tag Your Tickets and Make Them Easy to Find!

When it comes to your bug ticket queue, this is probably something you don't want to see:

<BrokenRobot> [Coke]: I've no idea how to locate them and now RT pissed me off enough that I gone off the idea of doing those tickets ~_~
...
<BrokenRobot> 18 minutes to find tickets. Retarded

And when a contributor is trying to find some bugs to fix, this is something they don't want to see:

This isn't the interface for the latest NASA space probe. It's the search page for the RT ticket system. And that's not even the full story, because if the contributor wants to find tagged tickets, they have to use "Advanced" version that is nothing more than a textbox asking for what may or may not be a SQL query.

Sure, if you've used it a few times, it becomes simple enough to use, but a new contributor who's beggining to learn the guts of your project probably doesn't want to spend their entire afternoon learning how to search for tickets. They probably wouldn't be too sure about what to search for anyway.

So what's the solution? Tags! Categorize your tickets with tags and then you can have a Wiki or page on your project's website linking to search queries for specific tags. Here are some ideas to consider when deciding on ticket tags:

For Core Member Review / Bikeshed

Consider this comment on a ticket:

This should probably return a List instead of a Hash.

The problem with it is that it's unclear whether or not there needs to be a discussion or if the ticket should be resolved following the suggestion. So you have one group of people who wait for more comments before doing anything and another group who agree with the suggestion and move on without further comments. In the end, you have a stalled ticket and nothing gets fixed.

For that reason, you should use more direct language in your comments. State explicitly whether you're just sharing your opinion/vote on the issue, inviting discussion, or giving direction for how the ticket should be resolved.

For core member reviews, discussions, and general bikeshed, tags can be very useful. For example, the Mojolicious project uses needs feedback Issue labels to invite discussion from its users. The Perl 6 ticket queue has [@LARRY] tag for tickets that need review by the core team members—and core members should review such tickets regularly, because as we've learned in the previous Lesson, some bugs are just a comment away from being fixed!

Area of Expertise

Different issues may require different expertise. For example, an issue with the Perl 6 documentation website may require someone experienced with:

  • Design: the look of the website
  • UX: usability of the website
  • CSS/HTML/JS: the front-end of the website
  • Perl 6: scripts for site generation
  • Advanced Perl 6: documenting arcane features
  • Pod6: documentation format writing and parsing
  • English: ensuring correct grammar and good writing style
  • Other languages: translation

All that just for a static website. Imagine one that's driven by a web app with a SQL backend!

Not everyone will be an expert in all the areas, so tagging by area of expertise will let a person pick tickets that are easy for them to resolve, depending on what sort of knowledge and training they possess.

Easy / Low Hanging Fruit

These are pretty common in projects: tags that indicate the ticket is very easy to solve. This is typically what new contributors will look for.

However, developers often mistake this tag to mean so easy a blind monkey could do it, and the result potential new contributors see when they view the tag is often:

Found 0 tickets

Easy tickets should not mean brain-dead tickets. Text editing is a task I often see listed as Low Hanging Fruit tickets, but it's booooring. New contributors don't want to get bored. They want to fix things. And there's nothing wrong with requiring to do a bit of learning while they're at it.

The tickets tagged as easy can simply have a comment narrowing down the potential hiding spot of the bug with, perhaps, some guidance on how to debug and fix it. And speaking of guidance...

LESSON 5: Mentoring Converts New Contributors into Experienced Contributors

So you have 20 devs on the team and 1,000 tickets in the bug queue. With 50 tickets per a pair of hands, you sure are busy! The devs never see the light of day and rarely appear on chat channels or comment on mailing lists, bug tickets, and other commentables. That's what busy people do.

However, what would happen if you also spend a bit of time writing tutorials and project internals courses, or training specific people?

<psch> i can also guide you how i figured out what to change where to throw a typed Exception there, because as i said, i think it's a great ticket for getting used to the Grammar and Actions
<BrokenRobot> psch: sure, I'd love to learn that
<psch> BrokenRobot: the most important thing is --target=parse
BrokenRobot: as in, the start for any bug that most likely is Grammar or
Actions based is by isolating it as far as possible - in this case a bare
'&0' - and running that through ./perl6 --target=parse -e
https://gist.github.com/peschwa/1e6a9f84a4c9e67638ff93e5b79f86d9 # like this
EXPR is a scary place, i don't go there
so it's about variable
<psch> https://github.com/rakudo/rakudo/blob/nom/src/Perl6/Grammar.nqp#L2017 so this
<BrokenRobot> So needs to exclude '&' on here?
https://github.com/rakudo/rakudo/bl​ob/nom/src/Perl6/Grammar.nqp#L2025

It's the old adage: teach a person to fish... and you'll turn a profit. Don't think of it as a team of 20 devs. Think of it as a team of 20 devs and several trainees. By dedicating some time on training new contributors you'll be growing your team and reducing the number of bugs per pair of hands. The time you won't spend on fixing bugs right now is an investment into people who will fix more bugs for you later on.

Conclusion

And so ends our date with the bug queue. Whether you're a newbie contributor or a seasoned core hacker, the bug queue is a valuable place to spend your time at.

New contributors help core developers by filtering out the queue, doing preliminary debugging, and writing tests. Core developers help new contribitors by tagging tickets, giving direction for how tickets should be resolved, and providing training. This in turn, lets new contributors help back by fixing bugs and... eventually becoming core contributors themselves.

A Date With The Bug Queue or Let Me Help You Help Me Help You

Read this article on Perl6.Party

Recently, I decided to undertake a quick little journey down to the Perl 6's Bug Queue. A quest for fame and profit—some easy game to hunt for sport. There's plenty of tickets, so how hard can it be? The quick little journey turned out to be long and large, but I've learned a lot of good lessons in the process.

PART I: The Newbie Contributor

Right away, I hit a snag. Some tickets looked hard. On some, it wasn't clear what the correct goal was. And some looked easy, but I wasn't sure whether I wanted to work on them just yet. While the ticket queue has the tag system, I needed some personal tags. Something special just for me....

The Ticket Trakr

So I wrote a nice little helper app—Ticket Trakr. It fetches all the tickets from the bug queue onto one page and lets me tag each of them with any combination of:

  • Try to fix
  • Easy
  • Tests needed
  • Needs core member decision
  • Needs spec decision
  • Check ticket later
  • Needs checking if it's still broken
  • Too hard
  • Not interested

The app worked great! I quickly started going through the queue, looking over the tickets, testing if the bugs were still there, and estimating whether I could and wanted to fix them. And after a full weekend of clicking, tagging, testing, closing, taking an occasional break to hunt bears with a mortar, more closing, testing, tagging, and clicking, I... was through just 200 tickets, which is only 15% of the queue:

And so I've learned the first lesson.

LESSON 1: Going Through Bug Reports is a Full Time Job

Whenever I see someone ask how they can contribute, the basket of offers they receive generally contains: docs, marketing, modules and libraries, or bug fixing. Going through the ticket queue doesn't seem to be considered a task on itself. The ticket queue is just a place where you find the bugs to fix, right?

What may not be obvious is the bug queue contains an extraordinary amount of work that can be performed by less skilled contributors to make it easier for highly-skilled—and by extension much scarcer—contributors to fix bugs. Let's see what those are:

Deciding On Whether The Report Should Be Accepted

Just because you have 1,000 tickets in your bug queue doesn't mean you have 1,000 bugs. Here are some things that might end up as a ticket and how you can help:

  • Poor bug reports: you should reply, asking for a decent test case or the missing information
  • Bug reports for an unrelated project: move them (or, for the lazy, just close with explanation)
  • Feature proposals: ping core members and users for discussion
  • A feature confused for a bug: explain the confusion; add to the documentation if this confusion happens often
  • Incorrectly used code that was never meant to work: offer a correct example; improve documentation, if needed
  • People asking for help with their code: redirect to appropriate help channels; improve documentation, if this happens often
  • Patches for other bugs: apply the patches, move them to the appropriate ticket, or make them easier to merge (e.g. make a Pull Request)
  • Duplicate bug reports: point out the dupe and close the report
  • Spam: grab some white bread and have a lunch break

This is a lot of work, but this is just the basics. What else can a person new to the project can contribute?

Debugging

So we've cleaned up our queue and now we have a few reports that appear to have at least some sort of a six-legged quality to them. Sure, we're new to the project and don't even know where to begin fixing them, but that doesn't mean we can't play around with code to narrow down the problem.

Reproduce the bug

Merely being able to reproduce the bug shows it's likely is indeed a bug and not just a quirk of the reporter's system. Speaking of systems: test the bug on several, especially if you have access to esoteric ones.

Test different versions of the project to see where the bug appeared. If possible, try to narrow down the commit that introduced the bug. For Rakudo Perl 6 compiler, you can use bisectable bot on IRC:

<Zoffix> bisect: say ^10 .grep: { last if * > 2 }
<bisectable> Zoffix: exit code is 0 on both starting points, bisecting by using the output
<bisectable> Zoffix: (2016-03-18) https://github.com/rakudo/rakudo/commit/6d120ca

Even if you can't fix the bug, all this extra information can help a more knowledgeable contributor. Especially if they are the author of the commit that introduced the bug.

Reduce the amount of code reproducing the bug

I've seen a disturbing amount of people playing code golf. Here is the perfect place to put those skills to good use: reproduce the bug with less code. This narrows down the areas where the bug is hiding.

For example, here's the original [actual] reported code, along with the bug report title:

# You can't catch Proc::Async failing because the external program
# doesn't exist if you open it for writing:

perl6 -e 'my $p; try {$p = Proc::Async.new(:w, "asdfcat"); CATCH {die "in
    new"}}; my $pr; try {$pr = $p.start; CATCH {die "in start"}}; try
    {await($p.write("hi\n".encode)); CATCH {die "in write"}}; try
    {$p.close-stdin; CATCH {die "in close"}}; try {await($pr); CATCH
    {die "in await"}}'

That code is a bitch to read, let's pop open an editor and format it properly:

my ( $p, $pr );
try { CATCH { die "in new"   }; $p  = Proc::Async.new: :w, "asdfcat" }
try { CATCH { die "in start" }; $pr = $p.start                       }
try { CATCH { die "in write" }; await $p.write: "hi\n".encode        }
try { CATCH { die "in close" }; $p.close-stdin                       }
try { CATCH { die "in await" }; await $pr                            }

# Outputs nothing when run

That's much better! So the report claims we can't catch things and we've got five try blocks and no output. Hmmm... Let's get rid of all the tries and catching and see what error the write throws:

given Proc::Async.new: :w, "asdfcat" {
    .start;
    await .write: "hi\n".encode;
}

# Outputs nothing when run

And the error is... nothing?! There's no output from the program, so maybe it "succeeds" in writing things and there's nothing to throw? Let's toss in a couple of say calls:

given Proc::Async.new: :w, "asdfcat" {
    say 'Starting';
    .start;

    say 'Writing';
    await .write: "hi\n".encode;

    say 'Wrote';
}

# OUTPUT:
# Starting
# Writing

The Wrote is missing from the output. The original report is incorrect in its assumptions that the issue is with the CATCH block! There's nothing to catch in the first place and the .write Promise seems to exit the program.

Perhaps, CATCH blocks were scary to you, but fixing a bug in a .write call is less so. And there you go: we found a contributor to fix the bug!

Write Tests For The Bug

So you've found out that it is a bug for sure and you've played around with code and maybe even found the commit that broke it. You don't know how to fix the bug, so are we done with this ticket then? How about we write tests!

After the bug is fixed, the project's test suite should contain a test checking regression of that bug. Perl's TAP supports TODO and SKIP features. TODO expects the test to fail and will alert you when it starts to pass. SKIP marks the needed number of tests as skipped and your test logic can avoid running them. So even if the bug is not yet fixed, we can still add tests for it—we'll just TODO or SKIP them, depending on how severe the bug is.

Perl 6's test suite has a fudging mechanism that lets you mark tests as skip or todo, depending on the compiler and virtual machine being tested:

#?rakudo.moar 2 todo 'Waiting fix for RT128526'
ok $fixed,      'stuff is fixed';
ok $also-fixed, 'other stuff is also fixed';

The test suite will expect the above two tests to fail for Rakudo compiler running on the MoarVM virtual machine and it will complain when they start to pass. If someone fixed the bug and wasn't aware of the open ticket, running the test suite alerts them of the ticket automatically. Magic!

As a bonus, the tests are the best description of the bug and they also can expose alternate failure modes that aren't apparent from the bug report itself:

<lizmat> .tell Zoffix the tests you added to S29-os/system.t yesterday hang on OSX  :-(

There's lots to do in the bug queue, but it's not as dull and boring at it appears at the first sight. And so, the bug queue taught me another lesson...

LESSON 2: The Bug Queue Is Not Thankless Labour

If you're a new contributor and you want to get up to speed, the bug queue isn't a bad place to start your learning. And there's a lot to learn from it!

Real-World Usage

You get to see real world code that uses the project you want to learn. Not the out-of-context code in the documentation. Not the polished code in the pre-made examples, but real-life, less-than-perfect, hacked-from-the-hip code. You also get to see code from multiple, vastly different people who use different style guidelines. Being able to easily read code regardless of the style used is a valuable skill.

Esoteric Features and Constructs

Everyone and their brother knows about and and what it's for, but did you know about andthen? I get flashbacks whenever I see it, but it's a useful chaining operator in Perl 6! And I learned about it from a bug report.

You can get very far with language primitives, but knowing more powerful and more appropriate contructs and features will let you write more concise and eloquent code. The bug queue can teach them to you.

Learning The Language

Writing tests can teach you the language you're writing them in. Writing tests for the Perl 6 compiler can teach you Perl 6. How much will you need to think when writing a program that runs a piece of code that might hang and in a set amount of time tells you whether the code resulted in success and kills the thread if it hung? I can do it with my eyes closed now.

Writing tests is a skill in itself and the bug queue gives you lots of opportunity for practice.

Getting To Know People

Going through the bug queue has a large social aspect to it as well:

  • Communicating with core members to find out whether and how a ticket should be resolved
  • Communicating with ticket creators (members of the community)
  • Having (hopefully amicable) discussions about features
  • Steering an overly-heated discussion to a more peaceful direction—bugs are annoying, and it's not uncommon for tickets to be written by pissed off people

Also, going through the ticket queue is not the favourite of the people, and the person who does it isn't exactly hated.

Conclusion

The bug queue is a much more complex beast than people may suspect. It has a lot to offer and a lot to teach. From language use, to the quirks of squishy humans. Today, we've examined the vast potential for contribution the ticket queue offers to people new to the project. Helping sift out actual bugs from false reports. Helping with debugging. Helping with tests.

That's not the only thing the bug queue has on the menu. In the next post, we'll examine what it has to teach more experienced regulars and core members of the project! How can you get more bugs fixed? Maybe the bug queue knows the answer.

About Zoffix Znet

user-pic I blog about Perl.