自动调用作为子例程引用的哈希值

时间:2016-05-05 22:51:42

标签: perl data-structures hash anonymous-function

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

为此,有没有办法在访问密钥时执行匿名子程序,而不使用任何特殊语法?这是一个简化的例子,说明了目标和问题:

#!/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)

7 个答案:

答案 0 :(得分:5)

As noted by Oleg,可以使用各种或多或少的神秘技巧(如tie,重载或魔术变量)来做到这一点。然而,这将是不必要的复杂和毫无意义的混淆。像这样的技巧一样酷,在实际代码中使用它们至少99%的时间都是错误的。

在实践中,最简单,最干净的解决方案可能是编写一个带有标量的辅助子程序,如果它是代码引用,则执行它并返回结果:

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

答案 1 :(得分:4)

我不相信其他人在反对tie mechanism时所写的字是有道理的。这些作者似乎都没有正确理解它是如何工作的以及可用的核心库备份

这是基于Tie::StdHash

tie示例

如果将哈希绑定到Tie::StdHash类,那么它就像普通哈希一样工作。这意味着除了您可能想要覆盖的方法

之外,没有什么可写的

在这种情况下,我已覆盖TIEHASH,以便我可以在与tie命令相同的语句中指定初始化列表,并FETCH调用超类&# 39; s 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并不是那么整洁,然后在声明点处然后插入值,因为必须将数据复制到绑定对象。但是,我在TIEHASH类中编写SpecialHash方法的方式使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

答案 2 :(得分:2)

是的,你可以。你可以tie哈希到实现,它会将coderefs解析为它们的返回值,或者你可以使用祝福的标量作为值overloaded mehods用于字符串化,数字化以及你想要自动解决的任何其他上下文。

答案 3 :(得分:2)

这种用例的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";
}

正如你所看到的 - 这可以让你选择一个普通的'标量,并用它做果味的东西。您可以使用与hash非常相似的机制 - 一个示例可能是进行数据库查找。

但是, 还需要非常谨慎 - 因为您通过这样做会在远处创建动作。未来的维护程序员可能不希望您的$random_var每次运行时实际更改,并且值分配实际上并未设置'。

它可能非常有用,例如虽然测试,这就是为什么我举一个例子。

在你的例子中 - 你可能会和你联系在一起。哈希:

#!/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";
}

这稍微不那么邪恶,因为未来的维护程序员可以使用你的哈希'一般。但是,动态评估可以在脚下射击不警惕,所以仍然 - 建议谨慎。

另一种方法是做到这一点'正确的'面向对象 - 创建一个'存储对象'那......基本上就像上面一样 - 只创建一个对象,而不是使用tie。这对于长期使用应该更加清晰,因为您不会获得意外行为。 (这是一个做魔术的对象,这是正常的,而不是一个有趣的哈希')。

答案 4 :(得分:2)

有一个名为" 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);

你也可以"修复"现有的哈希值。在哈希哈希场景中,

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

答案 5 :(得分:1)

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

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

请注意,您可以考虑使所有哈希值为subs(一个真正的调度表),而不是让一些返回非coderefs和一些返回refs。

但是,如果你这样定义哈希,那么在使用哈希时你不必做任何特别的诡计。它调用sub并在查找键时直接返回值。

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

答案 6 :(得分:0)

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

您可以使所有值代码引用,使哈希成为分派表,以进行统一调用

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;

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