初识Perl匿名函数
2013-07-23 11:37:42 阿炯

在计算机编程中,匿名函数(英语:anonymous function)是指一类无需定义标识符(函数名)的函数或子程序,普遍存在于多种编程语言中。匿名函数其实是个code reference,所以可以这样调用:
(sub { print "Hello freeoa\n" })->();
sub { print "Hello freeoa\n" }->();

或者也可以dereference后调用:
&{ sub { print "Hello freeoa" }}();

如果将code reference存到标量中,则可以被任意数据类型调用:
my $method = sub {...};

$obj->$method # same as $method->($obj)
$obj->$method(...) # $method->($obj, ...)

[1, 2, 3]->$method # $method->([1, 2, 3])
[1, 2, 3]->$method(...) # $method->([1, 2, 3], ...)
2->$method(); # $method->(2)
@arr->$method(); # $method->(@arr)
(sub { print 1;})->();

不只是定义匿名函数,还可以匿名定义其它数据类型。

定义匿名函数:
*anonymous_func = sub { printf("I'm an anonymous function\n"); } ;

可以通过 &anonymous_func来进行调用了。

定义匿名数组:
my ($anonymous_array_ref) = [ 1, 2, 3 ];
$anonymous_array_ref->[0];

定义匿名列表:
my ($anonymous_hash_ref) = { 'A' => 'Apple', 'B' => 'Boy' };
$anonymous_hash_ref->{'A'};

示例1

my $alias = sub {\@_}->(my ($x, $y, $z));
$x=$z=0;
$y=1;

print "@$alias"; # '0 1 0'

示例2

my $link=sub {
 my ($url,$name)=@_;
 print "$name->$url\n";
};

$link->('http://www.freeoa.net','FreeOA');

Otherwise, you would usually store an anonymous subroutine in a variable or data structure. The following calling styles work with both a variable and a sub {...} declaration:
dereference arrow:  sub {...}->(args)  or  $code->(args)
dereference sigil:  &{sub {...}}(args) or &$code(args)

可以构建匿名的对象有:
1.使用中括号[]构建匿名数组
2.使用大括号{}构建匿名hash
3.不包含任何元素的[]和{}分别是匿名空数组、匿名空hash

构造匿名对象

在数组、hash中构建匿名数组:
@name=('fairy',['zheng','freeoa','xiaohuan']);

%hash=('zheng' => ['male',18,'jiangxi'],
        'freeoa'     => ['male',20,'zhejiang'],
        'xiaohuan'  => ['female',19,'fujian'],
);

say "$name[1]"; # 输出ARRAY(0x...)
say "$hash{freeoa}"; # 输出ARRAY(0x...)

say "$name[1][2]";
say "$hash{freeoa}[1]";

如果不想在匿名数组中输入引号,可以使用qw(),与以下等价
@name=('fairy',['zheng','freeoa','xiaohuan']);
@name=('fairy',[qw(zheng freeoa xiaohuan)]);

在数组、hash中构建匿名hash:
@name=(    # 匿名hash作为数组的元素
       {# 第一个匿名hash
        'name'=>'zheng',
        'age'=>18,
        'prov'=>'jiangxi',
       },
       {# 第二个匿名hash
        'name'=>'freeoa',
        'age'=>20,
        'prov'=>'zhejiang',
       },
       {# 第三个匿名hash
        'name'=>'xiaohuan',
        'age'=>19,
        'prov'=>'fujian',
       },
);

%hash=(# 匿名hash作为hash的value
        'zheng'=>{# 第一个匿名hash
                      'gender'=>'male',
                      'age'=>18,
                      'prov'=>'jiangxi',
                     },
        'freeoa'=>{# 第二个匿名hash
                  'gender'=>'male',
                  'age' =>20,
                  'prov' =>'zhejiang',
                 },
        'xiaohuan'=>{# 第三个匿名hash
                     'gender'=>'female',
                     'age'   =>19,
                     'prov'  =>'fujian',
                    },
);

say "$name[2]"; # 输出HASH(0x...)
say "$hash[freeoa]"; # 输出HASH(0x...)

say "$name[2]{age}";
say "$hash{freeoa}{prov}";

再例如将一个匿名hash赋值给一个引用变量:
$ref_myhash = {
    name => 'Gilligan',
    hat => 'White',
    shirt => 'Red',
    position => 'First Mate',
};

为了后期维护方便,匿名数组、匿名hash中最后一个元素都使用了逗号。这个逗号并不会影响结果,但是却给未来修改匿名对象带来很大方便。

解除匿名对象的引用

从上面实验的结果中可以看到,当输出匿名对象时,其实输出的是个引用。

say "$name[1]"; # 输出ARRAY(0x...)
say "$hash{freeoa}"; # 输出ARRAY(0x...)

say "$name[2]"; # 输出HASH(0x...)
say "$hash[freeoa]"; # 输出HASH(0x...)

既然是引用,就可以解除引用,还原到数据对象:
1.正常情况下,使用@{数组引用}的方式解除数组引用,使用%{hash引用}的方式解除hash引用
2.所以使用@{匿名数组}解除匿名数组,使用%{匿名hash}解除匿名hash
3.注意,解除正常的数组、hash引用时,可以使用非规范的解除方式(去掉大括号,如@$ref_name),但是解除匿名对象的引用,必须不能去掉大括号
4.访问匿名对象中的元素和正常对象一样。一般没有必要去获取匿名对象中的元素,但是却有必要设置匿名对象中的元素时,后面介绍autovivification时将会看到这种行为

解除匿名数组对象,并获取匿名数组中的元素:
say "@{ ['zheng','xiaohuan','freeoa'] }"; # 解除匿名对象的引用
say "@{ [qw(zheng xiaohuan freeoa)] }"; # 解除匿名对象的引用
say "@{ [qw(zheng xiaohuan freeoa)] }[1]"; # 获取匿名对象中的第二个元素

解除匿名hash对象,并获取匿名hash中的元素:
$ref_hash={# 构造匿名hash,赋值给引用变量
    'zheng'=> ['male',18,'jiangxi'],
    'freeoa' => ['male',20,'zhejiang'],
    'xiaohuan' => ['female',19,'fujian'],      
};

@mykeys=keys %{ $ref_hash }; # 通过引用还原到匿名hash
say "@mykeys"; # 输出匿名hash中的键
say $ref_hash->{freeoa}[2]; # 输出匿名hash中匿名数组的某个元素

再例如,直接在需要hash的地方构建一个匿名hash,并解除引用。
@mykeys=keys %{# 解除匿名hash
    { # 构造匿名hash
    'zheng'=> ['male',18,'jiangxi'],
    'freeoa' => ['male',20,'zhejiang'],
    'xiaohuan' => ['female',19,'fujian'],
    }
};

say %{# 解除匿名hash
{ # 构造匿名hash
    'zheng'=> ['male',18,'jiangxi'],
    'freeoa' => ['male',20,'zhejiang'],
    'xiaohuan' => ['female',19,'fujian'],      
},};

如何区分匿名hash和一次性代码块

匿名hash使用大括号进行构建。但除了匿名hash,大括号还可以用来包围一堆语句,作为只执行一次的语句块。例如:
{
    my $name="zheng";
    my $prov="jiangxi";
}
# 出了语句块,上面两个my标记的变量就失效了

那么如何让perl知道大括号是用来构造匿名hash的,还是用来做一次性语句块的?大多数时候Perl根据上下文的环境会自动判断出来,但是有些时候无法判断,这时可以显式告诉Perl,这是匿名hash的构造符号,还是一次性语句块的符号:
1.大括号前面加上+符号,即+{...},表示这个大括号是用来构造匿名hash的
2.大括号内部第一个语句前,多使用一个;,即{;...},表示这个大括号是一次性语句块

+还可以加在匿名数组的中括号前,以及hash引用变量、数组引用变量前,而不仅仅是匿名hash的大括号前,如+$ref_hash、+[]、+$ref_arr。
@{ +[qw(zheng freeoa)]} # 匿名数组中括号前
@{ +$ref_arr } # 数组引用变量前
%{ +$ref_hash } # hash引用变量前

autovivification特性

这个单词竟然找不到对应的翻译,是perl自造的词,但却应用到了多种语言中:Wiki of Autovivification

根据它的功能的大概意思:当解除引用时,如果解除目标不存在,perl会自动创建一个空目标,而且自动创建时,会自动递归补齐上层。注意是解除引用时。类似于unix下的mkdir命令的-p选项一样,当创建某个目录的时候,如果它的父目录不存在,就会自动创建。

use 5.010;
push @{ $config{path} },'/usr/bin/perl';
say keys %config; # 输出:path
say $config{path}; # 输出:ARRAY(0x...)
say $config{path}[0]; # 输出:/usr/bin/perl

执行到push的时候,perl首先会发现@{}在解除一个引用,这个引用是$config{path},是一个hash引用,但是perl发现这个hash不存在,hash里的key(即path)也不存在,而且既然是push操作,说明这个key对应的value是个列表。于是perl的autovivification特性,首先会构建一个空的hash对象%config={},然后创建hash里的一个key:path,其值为空列表,即$config{path}=[],最后将"/usr/bin/perl"这个字符串push到对应的列表中,即$config{path}=['/usr/bin/perl']。

在上面的示例中,perl在解除引用时,自建了几个层次:
1.自建一个hash对象;
2.自建hash对象中的一个元素;
3.自建hash对象中某个元素的value部分。

必须注意,perl的autovivification功能只在解除引用的时候才自建,从解除引用的操作动机上看,当要解除引用,说明可能要操作引用对象中的数据了,那么缺少的部分应该要补齐。如果不是在解除引用,那么perl将根据语法特性决定是否自建对象。例如下面将自建数组@name和hash对象%person以及它的一个元素$person{name}。但这不是autovivification的特性,而是perl的语法特性。

push @name,"zheng";
$person{name}="zheng"
say "$name[0]";
say keys %person;

紧跟着上面的示例:
@{ $config{path} }[2]='/usr/bin/perl';

say $config{path}; # 输出:ARRAY(0x5571664403c0)
say $config{path}[0]; # 输出:空
say $config{path}[1]; # 输出:空
say $config{path}[2]; # 输出:/usr/bin/perl
say scalar @{$config{path}}; # 输出元素个数:3

子程序引用和匿名子程序

子程序也有引用和匿名子程序:
$ref_sub = \&mysub; # 子程序引用,&符号必须不能少
&{$ref_sub}(ARGS) # 解除子程序引用,传递参数ARGS
&$ref_sub(ARGS) # 解除子程序引用
$ref_sub->(ARGS) # 解除子程序引用
$ref_sub->() # 传递空参数

sub {...}; # 定义匿名子程序,sub后面没有名称
$ref_sub = sub {...}; # 匿名子程序的引用

有了子程序引用,就可以按需调用子程序:
sub java_learn {
    print "Learning Java now\n";
}
sub perl_learn {
    print "Learning Perl now\n";
}
sub python_learn {
    print "Learing Python now\n";
}

my %sub_hash=(
    "javaer"   => \&java_learn,
    "perler"   => \&perl_learn,
    "pythoner" => \&python_learn,
);

while(my ($who,$sub)=each %sub_hash){
    print "$who is learning\n";
    $sub->();
}

将匿名子程序作为数据结构的一部分:
use v5.32;

my %sub_hash = (
    "javaer" => sub {
        print "Learning Java now\n";
    },
    "perler" => sub {
        print "Learning Perl now\n";
    },
    "pythoner" => sub {
        print "Learning Python now\n";
    },
);

while( my($who,$sub)=each %sub_hash ){
    print "$who is learning\n";
    $sub->();
    say '-' x 32;
}

很多时候可能希望子程序的执行结果内插到双引号中,这时可以采用技巧"some string @{ [ mysub(arg1,arg2) ] } some string"的方式,将mysub的执行结果放进两端"some string"的中间。这是通过构建匿名列表,然后解除匿名数组实现的,因为数组是可以内插在双引号中的。

sub someone_say1{
    my $name=shift();
    if($name eq "jack"){
        print "Hello:",$name,"happy today!\n";
    }else{
        print "Hi:",$name," this is my home,welcome.\n";
    }
}

sub someone_say2{
    my $name=shift();
    if($name eq "back"){
        print "Hi,My name's:",$name," happy the day too.\n";
    }else{
        print "Thank you!\n";
    }
}

my %greets=(
    People_one =>  \&someone_say1,
    People_two =>  \&someone_say2,
);

for my $inst (qw(People_one People_two)){
        $greets{$inst}->('caodan');
        $greets{$inst}->('back');
}


子程序引用、匿名子程序的最大作用可能是用于回调函数(callback)、闭包(closure)。