Pretty Printing Perl 6

As I was working on Learning Perl 6, I wanted a way to pretty print a hash to show the reader what happened. I didn’t want to output from the builtin routines and a module I found was a good start but needed more work. So I created the PrettyDump module.

Before I get to my module, Perl 6 already has some nice ways to summarize objects. My first task was to dump a match object to see what it matched. Here’s a bit of code that matches a string against a regex and saves the result in $match. That’s a Match object:

my $rx = rx/ <[ a .. z ]> <[ 1 .. 9 ]> /;
my $string = ':::abc123::';
my $match = $string ~~ $rx;

put $match;

When I output that with put, I get the part of the string that matched:

c1

I could change the code slightly to use say. That’s like put but calls the .gist method on the object first to provide a human-compatible version of the object. Each object can decide on it’s own what that means.

say $match;  # put $match.gist

In this case, the output is almost the same. There are some fancy quotes around it:

「c1」

Instead of .gist, which say gives me for free, I could call the perl method explicitly.

put $match.perl;

This produces a string that represents what Perl 6 thinks the data structure is:

Match.new(list => (), made => Any, pos => 7, hash => Map.new(()), orig => ":::abc123::", from => 5)

I could also use dd, a Rakudo-specific dumping feature:

dd $match;

The output is similar to the string for .perl, but also slightly different:

Match $match = Match.new(list => (), made => Any, pos => 7, hash => Map.new(()), orig => ":::abc123::", from => 5)

I didn’t particularly like any of formats because they are squished together and rather ugly to my eyes (but being pleasing to me personally shows up in exactly zero designs). I looked for a module, and even though the Perl 6 module ecosystem is fairly young, I found Pretty::Printer from Jeff Goff:

use Pretty::Printer; # From Jeff Goff

my $rx = rx/ <[ a .. z ]> <[ 1 .. 9 ]> /;
my $string = ':::abc123::';
my $match = $string ~~ $rx;

Pretty::Printer.new.pp: $match;

When I tried this, I didn’t get anything (or, more exactly, I got literally “anything”):

Any

Pretty::Printer was nice for the few data types that it handled, but not a Match object. It had some builtin handlers that it selected with a given-when:

method _pp($ds,$depth)
  {
  my Str $str;
  given $ds.WHAT
    {
    when Hash    { $str ~= self.Hash($ds,$depth) }
    when Array   { $str ~= self.Array($ds,$depth) }
    when Pair    { $str ~= self.Pair($ds,$depth) }
    when Str     { $str ~= $ds.perl }
    when Numeric { $str ~= ~$ds }
    when Nil     { $str ~= q{Nil} }
    when Any     { $str ~= q{Any} }
    }
  return self.indent-string($str,$depth);
  }

I started to work on Pretty::Printer to add a Match handler, and then a few others, but I quickly realized I was getting far away from Jeff’s original code. Not only that, but I didn’t want to add more and more branches to the given-when:

method _pp($ds,$depth)
  {
  my Str $str;
  given $ds.WHAT
    {
    # Check more derived types first.
    when Match   { $str ~= self.Match($ds,$depth) }
    when Hash    { $str ~= self.Hash($ds,$depth)  }
    when Array   { $str ~= self.Array($ds,$depth) }
    when Map     { $str ~= self.Map($ds,$depth) }
    when List    { $str ~= self.List($ds,$depth) }
    when Pair    { $str ~= self.Pair($ds,$depth)  }
    when Str     { $str ~= $ds.perl }
    when Numeric { $str ~= ~$ds }
    when Nil     { $str ~= q{Nil} }
    when Any     { $str ~= q{Any} }
    }
  return self.indent-string($str,$depth);
  }

I changed my module name to PrettyDump and ended up with this:

use PrettyDump;

my $rx = rx/ <[ a .. z ]> <[ 1 .. 9 ]> /;
my $string = ':::abc123::';
my $match = $string ~~ $rx;

put PrettyDump.new.dump: $match;

I was much more pleased with the output which allowed me easily pick out the part of the object I wanted to inspect:

Match.new(
  :from(5),
  :hash(Map.new()),
  :list($()),
  :made(Mu),
  :orig(":::abc123::"),
  :pos(7),
  :to(7)
)

That solves that problem. But what about all the other types? One of my first improvements was a way to dump a class that my module did not know about. I knew about the TO_JSON method that the Perl 5 JSON module. With that, a class could decide its own JSON representation. I could do that with PrettyDump. If a class or object has a PrettyDump method, my module will use that preferentially:

class SomeClass {
    
    method PrettyDump ( $pretty, $ds, $depth ) {
        
        }
    }

my $pretty = PrettyDump.new;

my $some-object = SomeClass.new;

put $pretty.dump: $some-object;

The class doesn’t need to define that method. I could decorate an object with a PrettyDump method through a role. The but operator can do that for me by creating a new object in a new class that includes that role mixed into the original class:

use PrettyDump;

my $pretty = PrettyDump.new;

my Int $a = 137;
put $pretty.dump: $a;

my $b = $a but role {
  method PrettyDump ( $pretty, $depth = 0 ) {
    "({self.^name}) {self}";
    }
  };
put $pretty.dump: $b;

My code looks different from Jeff’s, but it’s not that different. Instead of a given-when, I have an if structure. I collapsed Jeff’s branches into self.can: $ds.^name to look for a matching method to the object type (and introduced a bug while doing it. See it?). The first branch looks for the PrettyDump method. The second does some special handling for numeric things. If none of those work, I die, which is another stupid thing I did at first.

method dump ( $ds, $depth = 0 ) {
  put "In dump. Got ", $ds.^name;
  my Str $str;

  if $ds.can: 'PrettyDump' {
    $str ~= $ds.PrettyDump: self;
    }
  elsif $ds ~~ Numeric {
    $str ~= self.Numeric: $ds, $depth;
    }
  elsif self.can: $ds.^name {
    my $what = $ds.^name;
    $str ~= self."$what"( $ds, $depth );
    }
  else {
    die "Could not handle " ~ $ds.perl;
    }

  return self.indent-string: $str, $depth;
  }

So, I kept going. I wanted a way to add (and remove) handlers to a PrettyDump object. I could add those as roles, but I thought about doing this repeatedly and often and didn’t like the idea of the frankenclass that would create. I added a way to do it on my own (although I might change my mind later):

my $pretty = PrettyDump.new;

class SomeClass {  }

my $handler = sub ( $pretty, $ds, Int $depth = 0 ) {
  ...
  }

$pretty.add-handler: 'SomeClass', $handler;

put $pretty.dump: $SomeClass-object;

My code added a couple more branches (and some code comments to elucidate the process). First, I’d look for a handler. If I’d defined one of those, I’d use it. Otherwise, I went through the same process. I did add some more checks at the end. If nothing else worked, I try a .Str method. Instead of die-ing at the end, I add an “unhandled thingy” string for that object. That way I know that I didn’t handle something and the rest of the program keeps going. That turned out to be more important than I thought. I use this to peek at a program as it executes. It’s not part of the program flow and shouldn’t interrupt it because my dumping code is incomplete:

method dump ( $ds, Int $depth = 0 --> Str ) {
  my Str $str = do {
    # If the PrettyDump object has a user-defined handler
    # for this type, prefer that one
    if self.handles: $ds.^name {
      self!handle: $ds, $depth;
      }
    # The object might have its own method to dump
    # its structure
    elsif $ds.can: 'PrettyDump' {
      $ds.PrettyDump: self;
      }
    # If it's any sort of Numeric, we'll handle it
    # and dispatch further
    elsif $ds ~~ Numeric {
      self!Numeric: $ds, $depth;
      }
    # If we have a method name that matches the class, we'll
    # use that.
    elsif self.can: $ds.^name {
      my $what = $ds.^name;
      self."$what"( $ds, $depth );
      }
    # If the class inherits from something that we know
    # about, use the most specific one that we know about
    elsif $ds.^parents.grep( { self.can: $_.^name } ).elems > 0 {
      my Str $str = '';
      for $ds.^parents -> $type {
        my $what = $type.^name;
        next unless self.can( $what );
        $str ~= self."$what"(
         $ds, $depth, "{$ds.^name}.new(", ')' );
        last;
        }
      $str;
      }
    # If we're this far and the object has a .Str method,
    # we'll use that:
    elsif $ds.can: 'Str' {
      "({$ds.^name}): " ~ $ds.Str;
      }
    # Finally, we'll put a placeholder method there
    else {
      "(Unhandled {$ds.^name})"
      }
    };

  return self!indent-string: $str, $depth;
  }

As I got further into this code, I looked at Perl 5’s Data::Dumper, but discovered that this isn’t the same sort of thing. That module outputs Perl code that I could eval to get back the same data structure. I didn’t want that can of worms in my module.

Beyond what I’ve shown here, I’ve been fiddling with formatting and other minor things as I run into problems. If there’s something that you’d like to do with the code, you can contribute through the PrettyDump GitHub repo, or even fork my code as the basis for your own experiments.

(Part of this work was supported by a travel grant from The Perl Foundation. I presented talks about my work at Amsterdam.pm, French Perl Workshop 2017, and London.pm.)


This article was originally posted on PerlTricks.com.

Tags

brian d foy

brian d foy is a Perl trainer and writer, and a senior editor at Perl.com. He’s the author of Mastering Perl, Mojolicious Web Clients, Learning Perl Exercises, and co-author of Programming Perl, Learning Perl, Intermediate Perl and Effective Perl Programming.

Browse their articles

Feedback

Something wrong with this article? Help us out by opening an issue or pull request on GitHub