Home > Raku > Sinking Errors

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.

Advertisement
Categories: Raku
  1. No comments yet.
  1. June 27, 2022 at 16:18

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: