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;
}

No comments: