美观打印 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.pm、2017年法国Perl研讨会和London.pm)做了关于我的工作的演讲。)
这篇文章最初发表在PerlTricks.com。
标签
brian d foy
brian d foy是一位Perl培训师和作家,也是Perl.com的高级编辑。他是Mastering Perl、Mojolicious Web Clients、Learning Perl Exercises的作者,也是Programming Perl、Learning Perl、Intermediate Perl和Effective Perl Programming的合著者。
查看他们的文章
反馈
这篇文章有什么问题吗?请通过在GitHub上打开问题或拉取请求来帮助我们。