美观打印 Perl 6

在我编写《学习 Perl 6》时,我希望有一种方法可以美观地打印哈希,以便向读者展示发生了什么。我不想使用内置例程和模块输出,我找到的模块是一个好的起点,但需要更多的工作。因此,我创建了PrettyDump模块

在我介绍我的模块之前,Perl 6已经有一些很好的方法来总结对象。我的第一个任务是转储一个匹配对象以查看它匹配了什么。以下是一段代码,它将字符串与正则表达式进行匹配,并将结果保存在$match中。这是一个匹配对象

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

put $match;

当我使用put输出时,我得到匹配的字符串部分

c1

我可以稍微修改一下代码,使用say。这就像put,但在调用对象上的.gist方法之前提供对象的人性化版本。每个对象都可以自己决定这意味着什么。

say $match;  # put $match.gist

在这种情况下,输出几乎相同。有一些花哨的引号包围着它

「c1」

而不是.gist,这是say免费提供的,我可以显式调用perl方法。

put $match.perl;

这产生了一个代表Perl 6认为的数据结构的字符串

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

我还可以使用dd,这是一个Rakudo特定的转储功能

dd $match;

输出与.perl的字符串类似,但略有不同

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

我不特别喜欢任何格式,因为它们都挤在一起,在我的眼里相当难看(但对我个人来说很满意的设计在零个设计中出现)。我寻找了一个模块,尽管Perl 6模块生态系统相对较年轻,但我找到了Jeff Goff的Pretty::Printer模块

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;

当我尝试这个时,我没有得到任何东西(或者更准确地说,我得到了字面上的“任何东西”)

Any

Pretty::Printer对于它处理的少量数据类型来说很好,但不能处理Match对象。它有一些内置的处理程序,它使用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);
  }

我开始为Pretty::Printer添加一个Match处理程序,然后添加了一些其他处理程序,但很快我就意识到我离Jeff的原始代码越来越远了。不仅如此,我还不希望向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);
  }

我将我的模块名称更改为PrettyDump,并得到了这个

use PrettyDump;

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

put PrettyDump.new.dump: $match;

我对输出感到非常满意,这使我能够轻松地选择我想要检查的对象部分

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

这解决了那个问题。但是,关于所有其他类型呢?我的第一个改进是让我的模块不知道的类可以转储。我知道Perl 5的JSON模块的TO_JSON方法。有了这个,类可以决定自己的JSON表示。我可以用PrettyDump做到这一点。如果一个类或对象有一个PrettyDump方法,我的模块将优先使用它

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

my $pretty = PrettyDump.new;

my $some-object = SomeClass.new;

put $pretty.dump: $some-object;

类不需要定义该方法。我可以通过一个角色用PrettyDump方法装饰一个对象。but操作符可以通过创建一个新的类来做到这一点,该类包含将角色混合到原始类中

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;

我的代码看起来和Jeff的不同,但其实并没有那么大的区别。我没有使用given-when结构,而是使用了if结构。我将Jeff的分支折叠成了self.can: $ds.^name来寻找与对象类型匹配的方法(在这个过程中引入了一个错误。看到了吗?)第一个分支寻找PrettyDump方法。第二个分支对数值类型进行了特殊处理。如果这些都不行,我就die,这是我最初犯的一个愚蠢的错误。

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

因此,我继续前进。我想找到一个方法来添加(和删除)PrettyDump对象的处理器。我可以把它们作为角色添加,但我考虑了反复这样做,并不喜欢创建出来的Franken类。我添加了一个方法来做这件事(尽管我以后可能会改变主意)

my $pretty = PrettyDump.new;

class SomeClass {  }

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

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

put $pretty.dump: $SomeClass-object;

我的代码添加了几个更多的分支(和一些代码注释来解释这个过程)。首先,我会寻找一个处理器。如果我定义了一个,我会使用它。否则,我会重复同样的过程。我在最后添加了一些额外的检查。如果什么都不行,我会尝试一个.Str方法。而不是在最后die,我添加了一个针对该对象的“未处理事物”字符串。这样我就能知道我没有处理某些东西,并且程序的其他部分会继续运行。这比我预想的要重要得多。我使用这个来检查程序在执行过程中的状态。这不是程序流程的一部分,不应该打断它,因为我的打印代码是不完整的。

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

当我深入研究这段代码时,我看了看Perl 5的Data::Dumper,但发现这不是同一类东西。那个模块输出Perl代码,我可以使用eval来获取相同的数据结构。我不想在我的模块中引入这个麻烦

除了这里展示的内容,我还一直在调整格式和其他一些小问题,因为我遇到了问题。如果您想用这段代码做些什么,您可以通过PrettyDump GitHub仓库进行贡献,或者甚至将我的代码作为您自己的实验的基础进行Fork。

(这项工作的一部分得到了Perl基金会提供的旅行资助。我在Amsterdam.pm2017年法国Perl研讨会London.pm)做了关于我的工作的演讲。)


这篇文章最初发表在PerlTricks.com

标签

brian d foy

brian d foy是一位Perl培训师和作家,也是Perl.com的高级编辑。他是Mastering PerlMojolicious Web ClientsLearning Perl Exercises的作者,也是Programming PerlLearning PerlIntermediate PerlEffective Perl Programming的合著者。

查看他们的文章

反馈

这篇文章有什么问题吗?请通过在GitHub上打开问题或拉取请求来帮助我们。