自动调用作为子程序引用的散列值

Automatically call hash values that are subroutine references

我有一个散列,其中有一些值不是标量数据,而是 return 标量数据的匿名子例程。我想让这对在散列中查找值的代码部分完全透明,这样它就不必知道某些散列值可能是 return 标量数据的匿名子例程不仅仅是普通的旧标量数据。

为此,有没有什么方法可以在访问匿名子例程的键时执行它们,而无需使用任何特殊语法?这是一个说明目标和问题的简化示例:

#!/usr/bin/perl

my %hash = (
    key1 => "value1",
    key2 => sub {
        return "value2"; # In the real code, this value can differ
    },
);

foreach my $key (sort keys %hash) {
    print $hash{$key} . "\n";
}

我想要的输出是:

perl ./test.pl
value1
value2

相反,这是我得到的:

perl ./test.pl
value1
CODE(0x7fb30282cfe0)

您需要确定代码引用何时存在,然后将其作为实际调用执行:

foreach my $key (sort keys %hash) {
    if (ref $hash{$key} eq 'CODE'){
        print $hash{$key}->() . "\n";
    }
    else {
        print "$hash{$key}\n";
    }
}

请注意,您可以考虑将所有哈希值设为 sub(真正的调度 table),而不是让一些 return 非代码引用和一些 return 引用。

但是,如果您这样定义散列,则在使用散列时不必做任何特殊的技巧。它在查找键时直接调用 sub 和 returns 值。

key2 => sub {
    return "value2";
}->(),

不,不是没有一些辅助代码。您要求一个简单的标量值和一个代码引用以相同的方式运行。这样做的代码远非简单,而且还会在您的哈希及其使用之间注入复杂性。您可能会发现以下方法更简单、更清晰。

您可以使所有值代码引用,使哈希成为 调度 table,用于统一调用

my %hash = (
    key1 => sub { return "value1" },
    key2 => sub {
        # carry on some processing ...
        return "value2"; # In the real code, this value can differ
    },
);

print $hash{$_}->() . "\n" for sort keys %hash;

当然,这种方法的开销很小。

是的,你可以。您可以使用 tie hash to implementation that will resolve coderefs to their return values or you can use blessed scalars as values with overloaded 方法来进行字符串化、谦化以及您想要自动解析的任何其他上下文。

,可以使用各种或多或少神秘的技巧来做到这一点,例如 tie、重载或魔术变量。然而,这将是不必要的复杂化和毫无意义的混淆。尽管这些技巧很酷,但在实际代码中使用它们至少在 99% 的情况下都是错误的。

实际上,最简单和最干净的解决方案可能是编写一个带有标量的辅助子例程,如果它是代码引用,则执行它并 returns 结果:

sub evaluate {
    my $val = shift;
    return $val->() if ref($val) eq 'CODE';
    return $val;  # otherwise
}

use it like this:

foreach my $key (sort keys %hash) {
    print evaluate($hash{$key}) . "\n";
}

Perl 针对此类用例的特殊功能之一是 tie。这允许您将面向对象的样式方法附加到标量或散列。

应谨慎使用,因为它可能意味着您的代码以意想不到的方式做着非常奇怪的事情。

但举个例子:

#!/usr/bin/env perl

package RandomScalar;

my $random_range = 10;

sub TIESCALAR {
    my ( $class, $range ) = @_;
    my $value = 0;
    bless $value, $class;
}

sub FETCH {
    my ($self) = @_;
    return rand($random_range);
}

sub STORE {
    my ( $self, $range ) = @_;
    $random_range = $range;
}

package main;

use strict;
use warnings;

tie my $random_var, 'RandomScalar', 5;

for ( 1 .. 10 ) {
    print $random_var, "\n";
}

$random_var = 100;
for ( 1 .. 10 ) {
    print $random_var, "\n";
}

如您所见 - 这让您可以使用 'ordinary' 标量,并用它做一些有趣的事情。您可以使用与 hash 非常相似的机制 - 一个示例可能是进行数据库查找。

但是,您需要非常谨慎 - 因为这样做是在远距离创建动作。未来的维护程序员可能不会期望您的 $random_var 每次 运行 都会实际更改,并且实际上不会 'set' 赋值。

它可以非常有用,例如。虽然正在测试,这就是我举个例子的原因。

在您的示例中 - 您可能 'tie' 哈希:

#!/usr/bin/env perl

package MagicHash;

sub TIEHASH {
    my ($class) = @_;
    my $self = {};
    return bless $self, $class;
}

sub FETCH {
    my ( $self, $key ) = @_;
    if ( ref( $self->{$key} ) eq 'CODE' ) {
        return $self->{$key}->();
    }
    else {
        return $self->{$key};
    }
}

sub STORE {
    my ( $self, $key, $value ) = @_;
    $self->{$key} = $value;
}

sub CLEAR {
    my ($self) = @_;
    $self = {};
}

sub FIRSTKEY {
    my ($self) = @_;
    my $null = keys %$self;    #reset iterator
    return each %$self;
}

sub NEXTKEY {
    my ($self) = @_;
    return each %$self;
}

package main;

use strict;
use warnings;
use Data::Dumper;

tie my %magic_hash, 'MagicHash';
%magic_hash = (
    key1 => 2,
    key2 => sub { return "beefcake" },
);

$magic_hash{random} = sub { return rand 10 };

foreach my $key ( keys %magic_hash ) {
    print "$key => $magic_hash{$key}\n";
}
foreach my $key ( keys %magic_hash ) {
    print "$key => $magic_hash{$key}\n";
}
foreach my $key ( keys %magic_hash ) {
    print "$key => $magic_hash{$key}\n";
}

这样就少了点邪恶,因为以后的维护程序员可以正常使用你的'hash'。但是动态评估可能会搬起石头砸自己的脚,所以还是要小心。

另一种方法是 'proper' 面向对象 - 创建一个 'storage object' ……基本上与上述类似 - 只是它创建了一个对象,而不是使用 tie。这对于长期使用来说应该更清楚,因为你不会得到意想不到的行为。 (这是一个正在施展魔法的对象,这很正常,而不是 'works funny' 的散列)。

我不认为其他人写的不赞成 tie mechanism 的话是有根据的。 None 的作者似乎正确理解它的工作原理以及可用的核心库备份

这是一个基于 Tie::StdHash

tie 示例

如果您将散列绑定到 Tie::StdHash class,那么它可以像普通散列一样工作。这意味着除了您可能想要覆盖的方法之外,没有什么可写的了

在这种情况下,我覆盖了 TIEHASH,这样我就可以在与 tie 命令相同的语句中指定初始化列表,而 FETCH 调用 superclass的FETCH,如果恰好是子程序引用则调用它

除了您要求的更改外,您的绑定哈希将正常工作。我希望很明显,如果您已将子例程引用存储为散列值,则不再有直接的方法来检索子例程引用。这样的值将始终被不带任何参数调用它的结果替换

SpecialHash.pm

package SpecialHash;

use Tie::Hash;
use base 'Tie::StdHash';

sub TIEHASH {
    my $class = shift;
    bless { @_ }, $class;
}

sub FETCH {
    my $self = shift;
    my $val = $self->SUPER::FETCH(@_);
    ref $val eq 'CODE' ? $val->() : $val;
}

1;

main.pl

use strict;
use warnings 'all';

use SpecialHash;

tie my %hash, SpecialHash => (
    key1 => "value1",
    key2 => sub {
        return "value2"; # In the real code, this value can differ
    },
);

print "$hash{$_}\n" for sort keys %hash;

输出

value1
value2



更新

听起来您的真实情况是使用看起来像这样的现有哈希

my %hash = (
    a => {
        key_a1 => 'value_a1',
        key_a2 => sub { 'value_a2' },
    },
    b => {
        key_b1 => sub { 'value_b1' },
        key_b2 => 'value_b2',
    },
);

在已经填充的变量上使用 tie 不如在声明点绑定 then 然后插入值那么整洁,因为必须将数据复制到绑定的对象。然而,我在 SpecialHash class 中编写 TIEHASH 方法的方式使得在 tie 语句

中的操作变得简单

如果可能,最好先tie每个散列,然后再将数据放入其中并添加它到主哈希

该程序将 %hash 中恰好是散列引用的每个值联系起来。这里面的核心就是语句

tie %$val, SpecialHash => ( %$val )

其功能与

相同
tie my %hash, SpecialHash => ( ... )

在前面的代码中取消引用 $val 以使语法有效,并且还使用散列的当前内容作为绑定散列的初始化数据。这就是数据被复制的方式

之后只有几个嵌套循环转储整个 %hash 以验证关系是否有效

use strict;
use warnings 'all';
use SpecialHash;

my %hash = (
    a => {
        key_a1 => 'value_a1',
        key_a2 => sub { 'value_a2' },
    },
    b => {
        key_b1 => sub { 'value_b1' },
        key_b2 => 'value_b2',
    },
);

# Tie all the secondary hashes that are hash references
#
for my $val ( values %hash ) {
    tie %$val, SpecialHash => ( %$val ) if ref $val eq 'HASH';
}

# Dump all the elements of the second-level hashes
#
for my $k ( sort keys %hash ) {

    my $v = $hash{$k};
    next unless ref $v eq 'HASH';

    print "$k =>\n";

    for my $kk ( sort keys %$v ) {
        my $vv = $v->{$kk};
        print "    $kk => $v->{$kk}\n" 
    }
}

输出

a =>
    key_a1 => value_a1
    key_a2 => value_a2
b =>
    key_b1 => value_b1
    key_b2 => value_b2

有一个名为 "magic" 的功能允许在访问变量时调用代码。

向变量添加魔法会大大减慢对该变量的访问,但有些比其他的更昂贵。

  • 无需访问散列的每个元素,只需访问一些值即可。
  • tie 是一种更昂贵的魔法形式,这里不需要它。

因此,最有效的解决方案如下:

use Time::HiRes     qw( time );
use Variable::Magic qw( cast wizard );

{
   my $wiz = wizard(
      data => sub { my $code = $_[1]; $code },
      get => sub { ${ $_[0] } = $_[1]->(); },
   );

   sub make_evaluator { cast($_[0], $wiz, $_[1]) }
}

my %hash;
$hash{key1} = 'value1';
make_evaluator($hash{key2}, sub { 'value2@'.time });

print("$hash{$_}\n") for qw( key1 key2 key2 );

输出:

value1
value2@1462548850.76715
value2@1462548850.76721

其他示例:

my %hash; make_evaluator($hash{key}, sub { ... });
my $hash; make_evaluator($hash->{$key}, sub { ... });

my $x; make_evaluator($x, sub { ... });
make_evaluator(my $x, sub { ... });

make_evaluator(..., sub { ... });
make_evaluator(..., \&some_sub);

您还可以 "fix up" 现有哈希。在您的哈希方案中,

my $hoh = {
   { 
      key1 => 'value1',
      key2 => sub { ... },
      ...
   },
   ...
);

for my $h (values(%$hoh)) {
   for my $v (values(%$h)) {
      if (ref($v) eq 'CODE') {
         make_evaluator($v, $v);
      }
   }
}