Sunday, September 6, 2009

Print-and-log in Perl 6

In one of my early blog entries, "Simple print-and-log subroutine", I shared a small piece of code that has been a nice, every-day tool - in Perl 5.

Today, I set about converting that to a naïve Perl 6 version, and being quite the Perl 6 n00b still, I ran into a few hurdles along the way.

The hurdles were easy enough to avoid, once masak++ and moritz++ had bonked my head sufficiently.

I'll walk through the code, step by step, to illustrate what I learned today, and what others might need to know.
use v6;
Ah, first, remember this statement. It's a nice hint for other software if you want to continue using the .pl file suffix instead of .p6l etc.
my $verbose = 1;
my $logfile = "/tmp/test.log";

my $*PREFIX = "plog";
Variables starting with the twigil $* is what we call contextual variables, and they just started working yesterday in Rakudo. Contextual variables follow a dynamic call path. When we reuse this variable later, it will depend on which call path we followed.
sub plogwarn ($msg)
{
my $*PREFIX = "plogwarn";
$*PREFIX now has a different value only for the cases where we've called the plogwarn subroutine, and the subsequent call to plog inherits this value.
  plog $msg,1;
Oh, BTW, it's really, REALLY important to keep track of where you add whitespace or not. I'd been sleeping and class, and forgotten completely that plog($msg,1) would call plog with the parameters $msg and $1, while plog ($msg,1) would call plog with the single parameter ($msg,$1) (yep, a list). It may be safer to avoid parentheses altogether when you're not dealing with lists - except when you have to.
}

sub plogdie ($msg)
{
my $*PREFIX = "plogdie";
plog $msg,2;
exit 1;
}

sub plog ($msg is copy, $level?)
The trait is copy means that I want to make a copy of the incoming parameter $msg, so that I can modify it inside of plog. Normally, parameters in Perl 6 are read-only and pass-by-reference (though not "reference" as you know it from Perl); the parameter as named is merely an alias for the actual variable. I can change the parameter in the caller if I use is rw, but that's not what I want to do here. The question mark in the second parameter - $level? - means that the parameter is optional, and I've used that in both plogwarn and plogdie above.
{ 
my $dt = Time.gmtime.date.iso8601
~ " "
~ Time.gmtime.time.iso8601;
Normally, I'd use localtime, but this is NYI (not yet implemented) in Temporal.pm.

$msg = ($dt,"[$*PREFIX]",$msg).join(" ");
Here, I abuse that is copy to save myself one precious variable, just as an example.

if $verbose {
given $level {
when 1 { warn $msg; }
when 2 { die $msg; }
default { say $msg; }
}
The given…when construct is similar to the switch…case construct in other languages, but subtly different, as it allows more possible values and value types than most. Coming from a world of Perl 5.8 and older, this is simply lovely.
  }
# Append message to $logfile
my $*OUT = open $logfile, :a;
Here's another use of a contextual variable, but this time it's the one normally associated with STDOUT. print FILEHANDLE $msg is no longer necessary, because you can always assign $*OUT in its own scope. The open statement has :a to indicate that I'm opening for append (so the syntax is less unixy than Perl 5's >>).
  say $msg;
$*OUT.close;
}
This is how we can use the three subroutines, and the output should illustrate the different contextuals nicely:

plog("Oh, hello, there, sweetie.");
plogwarn("Consider yourself warned");
plogdie("Die, frackin' monster, die!");
…outputs…
2009-09-06 19:59:32 [plog] Oh, hello, there, sweetie.
2009-09-06 19:59:32 [plogwarn] Consider yourself warned
2009-09-06 19:59:32 [plogdie] Die, frackin' monster, die!

And here's the complete example code, which works with the current Rakudo:
use v6;

my $verbose = 1;
my $logfile = "/tmp/test.log";
my $*PREFIX = "plog";

sub plogwarn ($msg)
{
my $*PREFIX = "plogwarn";
plog $msg,1;
}

sub plogdie ($msg)
{
my $*PREFIX = "plogdie";
plog $msg,2;
exit 1;
}

sub plog ($msg is copy, $level?)
{
my $dt = Time.gmtime.date.iso8601
~ " "
~ Time.gmtime.time.iso8601;

$msg = ($dt,"[$*PREFIX]",$msg).join(" ");

if $verbose {
given $level {
when 1 { warn $msg; }
when 2 { die $msg; }
default { say $msg; }
}
}
# Append message to $logfile
my $*OUT = open $logfile, :a;
say $msg;
$*OUT.close;
}

Monday, August 31, 2009

Some ways that Perl 6 is grand, part 2 of ?

Okay, this is really part 1b of ?, but…

In my earlier post, I used the zip operator to join two lists into a hash.

There was one obvious use of the operator that escaped me at the time, and that was how I sometimes need to create a new hash from the keys of two hashes, or keys and values. And now I think it's starting to look neat:
my %A = { a => 1, b => 2 };
my %B = { z => 9, y => 8 };

my %AB = %A.keys Z %B.keys;
# { "a" => "z", "b" => "y" }

%AB = %A.keys Z %B.values;
# { "a" => 9, "b" => 8 }
However, this is a bit unpredictable, since the hash key order is undefined. So if you expect sorted keys, do that at the same time:
%AB = %A.keys.sort Z %B.keys.sort;
# { "a" => y, "b" => z }

# Sort by B's values - two variants
%AB = %A.keys.sort Z map { %B{$_} }, %B.keys.sort;
%AB = %A.keys.sort Z %B.sort.map( { .value } );
# { "a" => 8, "b" => 9 }
The equivalent Perl 5.10 version would be:
use List::MoreUtils qw/zip/;
my @k = sort(keys(%A));
my @v = map { $B{$_} }, sort(keys(%B));
%AB = zip @k, @v;

I now have a nice-ish argument for upgrading to Perl 5.10.1 on $workplace's servers. :D

masak++ for helping a tired me with the map expression.
Chas. Owens++ for spotting the missing use statement for Perl 5.
Pm++ for another way of sorting by value, just what I was hoping for!
isec++ for spotting a missing sort() for Perl 5.

Sunday, August 23, 2009

Autovivification - a reminder

Most of you already know this by heart, but the odd reader may have forgotten.

Autovivification is what we call the process of automatically creating entries in built-in data structures (Perl 5: array/list and hash), usually at the time we check whether an inner element exists or not.

This can be a royal PITA, if you don't pay attention to the problem. That's why it keeps being mentioned.

Here's a simple example:

my %hash;
my $n;
while (!exists ($hash{x}) && $n < 5) {
$n++;
if (!exists ($hash{x}{y}) {
print "hash{x}{y} does not exist: $n\n";
}
}

Q: How many times does the above while loop run in Perl 5?
A: Once.

The simple matter of checking the existence of the inner hash resulted in an entry being created for $hash{x}.

That means that tests like these should be written more carefully:


if (defined ($hash{x})) {
if (!exists ($hash{x}{y}) {
print "$n: hash{x}{y} does not exist.\n";
}
} else {
print "$n: hash{x} is undefined.\n";
}

This prints $n: hash{x} is undefined. (with an incrementing $n) five times.


Edit 2009-08-24 18:49 UTC: MST commented that there is an autovivification module on CPAN that lets us say no autovivification; - and it's even lexically scoped! That's just $notreallyanexpletive brilliant! Thanks, Matt, and thanks, Vincent!


Oh, BTW, Perl 6 has a useful specification for autovivification, which illuminates the problem further.