Sinking Errors
I was looking for a way to output debug messages that can also carry additional values, when not output to the screen. That is easy. The tricky part is golfing the interface. After quite a bit of struggle. I ended up with the following.
use Log;
dd ERROR('foo') ~~ LogLevel::ERROR;
dd ERROR('foo') ~~ LogLevel::DEBUG;
say ERROR('foo').&{ .file, .line };
VERBOSE 'Detailed, very much so indeed.';
my $*LOGLEVEL = 2;
VERBOSE 'Detailed, very much so indeed.';
# OUTPUT:
# Bool::True
# Bool::False
# (2021-03-08.raku 1661)
# 2021-03-08.raku:1664 Detailed, very much so indeed.
In sink-context, the “functions” will output to $*ERR
and when not sunk return an object that provides a few useful methods. To get both, we need to implement the methods sink
and CALL-ME
.
# Log.rakumod
use v6.d;
my role Functor {
has $.message;
my $.level;
has $.captured-loglevel;
has $.file;
has $.line;
method CALL-ME(*@message) {
my ($line, $file) = callframe(1).&{ .line, .file };
self.new: message => "$file:$line " ~ @message.join($?NL), :captured-loglevel($*LOGLEVEL // 0), :$line, :$file;
}
method sink { $*ERR.put(self.message) if ($!captured-loglevel // 0) ≥ $.level }
method Str(::?CLASS:D:) { self.message }
method gist(::?CLASS:D:) { self.^shortname ~ ' ' ~ self.message }
}
my $ERROR; my $VERBOSE;
my $DEBUG;
class LogLevel {
constant ERROR := $ERROR;
constant VERBOSE := $VERBOSE;
constant DEBUG := $DEBUG;
}
$ERROR = class :: is LogLevel does Callable does Functor { my $.level = 1; }
$VERBOSE = class :: is LogLevel does Callable does Functor { my $.level = 2; }
$DEBUG = class :: is LogLevel does Callable does Functor { my $.level = 3; }
sub EXPORT {
Map.new: '&ERROR' => $ERROR,
'&VERBOSE' => $VERBOSE,
'&DEBUG' => $DEBUG,
'LogLevel' => LogLevel,
}
To behave like a function a Callable
must be bound to an &
-sigiled symbol (or Rakudo complains when we use it as a term). As soon as Rakudo spots a class, it will stuff it into UNIT::EXPORT::ALL
. When importing any &
-sigiled key of sub EXPORT
, with the same basename as a class with that basename, the symbol from EXPORT
will be ignored. The only way to avoid that is to have anonymous classes and do the forward-declarations by hand. The latter is needed to allow ERROR('foo') ~~ LogLevel
.
All that is a bit convoluted but works as expected. What I didn’t expect, is that dynvars that are user-defined are not visible in method sink
. Not much of a problem in my case, as capturing the state of $*LOGLEVEL
inside CALL-ME
is the right thing to do anyway. What threw me off is the inconsistency with other functions and methods that are called by a time-machine. We type BEGIN
, BUILD
and friends in all-caps for that very reason. This may warrant a problem solving issue. It’s an ENODOC for sure.
I believe golfing APIs is a most desireable effort. Laziness isn’t just a virtue for programmers, often it’s a given.
-
June 27, 2022 at 16:182022.26 Conference Seasoned – Rakudo Weekly News