我已经看到可以以功能或OO方式使用的CPAN Perl模块。我通常根据需要编写OO和Functional程序包,但是我仍然不怎么写可以同时使用两种方法的模块。
有人可以给我一个可以以功能和/或面向对象方式使用的软件包的简单示例吗?我显然对允许两种包装都使用的件很感兴趣。
谢谢
答案 0 :(得分:3)
一个核心示例是File::Spec,它有一个File::Spec::Functions包装器。它并不是面向对象的,而是使用了面向对象的继承原理,因此其主要API使用了方法调用,但是不需要保持任何状态。
use strict;
use warnings;
use File::Spec;
use File::Spec::Functions 'catfile';
print File::Spec->catfile('/', 'foo', 'bar');
print catfile '/', 'foo', 'bar';
另一个示例是Sereal,其编码器和解码器既可以用作对象,也可以通过包装它们的导出函数来使用。
use strict;
use warnings;
use Sereal::Encoder 'encode_sereal';
my $data = {foo => 'bar'};
my $encoded = Sereal::Encoder->new->encode($data);
my $encoded = encode_sereal $data;
撇开现实,将对象类和导出模块分开是通常的良好组织习惯。尤其不要尝试使同一个函数可以作为方法或导出函数来调用;主要问题在于,无论该子程序被称为$obj->function('foo')
还是function($obj, 'foo')
,它都与子例程本身没有区别。正如@choroba指出的那样,CGI.pm试图做到这一点,这是一团糟。
答案 1 :(得分:2)
我的WiringPi::API发行版是这样写的。请注意,在这种情况下,不需要保存状态,因此,如果必须保持状态,则这种保存方式将无法按原样工作。
您可以在功能上使用它:
use WiringPi::API qw(:all)
setup_gpio();
...
或使用其面向对象的界面:
use WiringPi::API;
my $api = WiringPi::API->new;
$api->setup_gpio();
...
对于功能,我使用@EXPORT_OK
,以便不会不必要地污染用户的名称空间:
our @EXPORT_OK;
@EXPORT_OK = (@wpi_c_functions, @wpi_perl_functions);
our %EXPORT_TAGS;
$EXPORT_TAGS{wiringPi} = [@wpi_c_functions];
$EXPORT_TAGS{perl} = [@wpi_perl_functions];
$EXPORT_TAGS{all} = [@wpi_c_functions, @wpi_perl_functions];
...以及一些示例函数/方法。本质上,我们检查传入的参数数量,如果有多余的参数(将是类/对象),则手动shift
手动将其删除:
sub serial_open {
shift if @_ > 2;
my ($dev_ptr, $baud) = @_;
my $fd = serialOpen($dev_ptr, $baud);
die "could not open serial device $dev_ptr\n" if $fd == -1;
return $fd;
}
sub serial_close {
shift if @_ > 1;
my ($fd) = @_;
serialClose($fd);
}
sub serial_flush {
shift if @_ > 1;
my ($fd) = @_;
serialFlush($fd);
}
通常,我会做一些参数检查以确保我们转移了正确的东西,但是在测试中,允许后端C / XS代码为我担心的速度更快。
答案 2 :(得分:1)
如前所述,有许多模块可以做到这一点,其中一些已经被命名。
一个好的做法是为功能接口编写一个单独的模块,该模块use
对该类进行操作并照此导出其(选择)功能。
但是,如果有特殊需要,可以在一个程序包中同时使用相同的方法/函数名称来包含两个接口。请参阅最后一节,了解一个非常的罕见案例,以下基本示例将无法处理,以及如何解决。
这是具有两个接口的基本软件包
package Duplicious; # having interfaces to two paradigms may be confusing
use warnings;
use strict;
use feature 'say';
use Scalar::Util qw(blessed);
use Exporter qw(import);
our @EXPORT_OK = qw(f1);
my $obj_cache; # so repeated function calls don't run constructor
sub new {
my ($class, %args) = @_;
return bless { }, $class;
}
sub f1 {
say "\targs in f1: ", join ', ', @_; # see how we are called
my $self = shift;
# Functional interface
# (first argument not object or class name in this or derived class)
if ( not ( (blessed($self) and $self->isa(__PACKAGE__))
or (not ref $self and $self->isa(__PACKAGE__)) ) )
{
return ($obj_cache || __PACKAGE__->new)->f1($self, @_);
}
# Now method definition goes
# ...
return 23;
}
1;
来电者
use warnings; # DEMO only --
use strict; # Please don't mix uses in the same program
use feature 'say';
use Duplicious qw(f1);
my $obj = Duplicious->new;
say "Call as class method: ";
Duplicious->f1("called as class method");
say "Call as method:";
my $ret_meth = $obj->f1({}, "called as method");
say "\nCall as function:";
my $ret_func = f1({}, "called as function");
我发现原则上在定义类的模块中使用Exporter
很尴尬(但是我不知道这样做有任何实际问题);这会导致潜在的混乱界面。这本身就是分离接口的一个好理由,以便功能性的必须加载特定的模块。
还有一个细节需要引起注意。方法调用
($obj_cache || __PACKAGE__->new)->f1(...)
使用缓存的$obj_cache
(如果已经调用了该子项)进行调用。因此,保留了对象的状态,该状态可以在先前对f1
的调用中进行或未进行过处理。
在旨在用于非面向对象的上下文中的调用中,这相当重要,应该仔细研究。如果存在问题,请删除该缓存或将其扩展为完整的if
语句,在该语句中可以根据需要重置状态。
这两个用途绝对不能在同一程序中混用。
输出
Call as class method: args in f1: Duplicious, called as class method Call as method: args in f1: Duplicious=HASH(0x21b1b48), HASH(0x21a8738), called as method Call as function: args in f1: HASH(0x21a8720), called as function args in f1: Duplicious=HASH(0x218ba68), HASH(0x21a8720), called as function
函数调用分派给该方法,因此分两行(注释参数)。
为了测试派生类,我使用最小
package NextDupl;
use warnings;
use strict;
use feature 'say';
use parent 'Duplicious';
1;
并添加到下面的主程序中
# Test with a subclass (derived, inherited class)
my $inh = NextDupl->new;
say "\nCall as method of derived class";
$inh->f1("called as method of derived class");
# Retrieve with UNIVERSAL::can() from parent to use by subclass
my $rc_orig = Duplicious->can('f1');
say "\nCall via coderef pulled from parent, by derived class";
NextDupl->$rc_orig("called via coderef of parent by derived class");
附加输出是
Call as method of derived class args in f1: NextDupl=HASH(0x11ac720), called as method of derived class Call via coderef pulled from parent, by derived class args in f1: NextDupl, called via coderef of parent by derived clas
它包含在注释中出现的使用UNIVERSAL::can
的测试。
在评论中提出并讨论了一个具体的限制(我知道)。
想象一下,我们编写了一个方法,该方法将对象(或类名)作为其第一个参数,因此将其作为->func($obj)
来调用;更重要的是-这很重要-此方法允许 any 类,因为它的工作方式并不关心它具有的类。这将是非常特殊的,但是它是可能的,并且会引发以下问题。
与该方法相对应的函数调用为func($obj)
,并且当$obj
恰好位于此类的层次结构中时,将导致方法调用->func()
,错误地。
在确定是否是否存在的代码中没有办法消除歧义 它被称为函数或方法,因为它所做的只是看第一个参数。如果它是我们自己层次结构中的一个对象/类,它会确定这是对该对象的方法调用(或类方法调用),在这种情况下是错误的。
模块的作者可以通过两种简单的方法来解决这个问题
不为这种高度特定的方法提供功能界面
给它一个单独的(明确相关的)名称
通过检查第一个参数来决定如何调用我们的if
条件是固定的,但仍然为具有该接口的每个方法编写。因此,在此方法中,再检查一个参数:如果第一个是该类的对象/类,而第二个是(任意)对象/类,则为方法调用。 如果第二个参数为可选,则此方法无效。
所有这些都是完全合理的。在行使其定义特征,拥有和使用数据(“属性”)的类中,可能会有一些方法无法转换为函数调用。这是因为单个程序只应使用 one 接口,并且具有函数的状态是没有状态的,因此依赖它的方法将不会运行。 (为此使用缓存对象非常危险。)
因此,人们总是必须仔细决定接口,然后选择。
感谢Grinnz的评论。
请注意,“函数式编程”有一个完全不同的范例,标题有些不清楚。所有这些都与程序性方法中的功能接口有关。