【问题标题】:Make package both Functional and OO使包功能和面向对象
【发布时间】:2019-03-16 16:36:23
【问题描述】:

我见过可以以函数式或 OO 方式使用的 CPAN Perl 模块。我通常会根据自己的需要编写OO和Functional包,但我仍然不会编写可以同时使用这两种方式的模块。

有人可以给我一个简单的示例,说明可以以功能和/或 OO 方式使用的包吗?我显然对可以双向使用包的部分感兴趣。

谢谢

【问题讨论】:

  • CPAN 上有很多例子。这一切都只是代码。你可以在那里阅读。随意采取高峰。 :)
  • 最好不要。制作一个 OO 包,第二个包以功能样式导出 OO 模块,例如 Object::Import (metacpan.org/pod/Object::Import)。
  • @choroba blargh :D
  • 您要问的问题是如何允许程序样式的用户界面与面向对象的样式界面共存于模块中。函数式编程是一种完全不同的范式。

标签: perl package cpan


【解决方案1】:

我的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 代码为我担心会更快。

【讨论】:

  • @simbabque 不。这是一个很好的观点。完全忽略了这一点。我将用该声明更新答案。
  • 只需将shift if @_ > 1; 替换为my $self = @_ > 1 ? shift : get_global_object();
  • @ikegami 谢谢,一如既往。真是太好了!
  • 这种方法可以防止变长参数列表。我想我会错过的。可以改为检查第一个参数(例如,像 CGI.pm 那样)
  • @ikegami 是的,确实如此。不过,这几乎是一个将参数传递给 C 并忘记的方法,老实说,这是我完成的第一个真正的 C 库包装器之一。因为它已经稳定了几年左右,所以我没有重新访问(但是从那时起我深入研究了 perlxs、perlguts 和 perlcall,所以这可能是让这段代码更有活力的好时机)。
【解决方案2】:

一个核心示例是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;

除了Sereal,将对象类和导出模块分开通常是一种很好的组织实践。特别是不要试图让相同的函数作为方法或导出函数可调用;主要问题是它与子例程本身无法区分,无论它被称为$obj->function('foo') 还是function($obj, 'foo')。正如@choroba 所说,CGI.pm 试图这样做,结果一团糟。

【讨论】:

  • 至于“与子例程本身无法区分”——正确,但没有方法期望自己的对象作为参数(它已经有了);该示例使用不合理(或几乎不可能)。我同意这是一种更清洁、更安全的方法。但是您没有显示实现(仅使用已知模块中的示例)-它不是那么干净和无故障,这就是我认为我们有这个问题的原因。
  • 那个例子是错误的。我不是要攻击你的帖子,但这很重要。 $obj->func('foo') 的等效项是调用 func('foo'),而不是您显示的调用 func($obj, 'foo')。那只是一个错误,错误地使用了一个功能。也许您的意思是提出错误是如何发生的问题?但这些都是公海——总是可以用错误的输入来破坏代码(如果你知道源代码,通常是微不足道的)。
  • @zdim 您似乎误解了。从子程序的角度来看,$obj->func('foo')func($obj, 'foo') 的结果与 @_ 完全相同(尽管第一个可能由于方法分派而最终调用不同的类)。
  • 等一下——对func() 的子调用是一个彻底的错误。如果方法func 期望'foo' 作为它的第一个参数,那么同一模块中的函数func 必须期望'foo' 作为它的第一个参数。呼叫func($obj, 'foo') 是一个错误。如果您认为第一个参数可能是某个其他类的对象,那么可以轻松地将其作为函数调用正确分派(请参阅我的答案)。
  • @zdim 我说的是Perl的实现,不是具体子程序的API。
【解决方案3】:

如上所述,有许多模块可以执行此操作,其中一些已命名。一个好的做法是为功能接口编写一个单独的模块,uses 类并导出它的(选择)函数。

但如果有特殊需要,很有可能将两个接口放在一个包中,并具有相同的方法/函数名称。有关以下基本示例无法处理的一个非常特定且罕见的用例,以及如何解决它,请参阅最后部分。

这是一个具有两个接口的基本包

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

输出

作为类方法调用: f1 中的 args:重复,称为类方法 作为方法调用: f1中的args:Duplicious=HASH(0x21b1b48), HASH(0x21a8738),被称为方法 作为函数调用: f1 中的 args:HASH(0x21a8720),作为函数调用 f1 中的 args:Duplicious=HASH(0x218ba68), HASH(0x21a8720),作为函数调用

函数调用分派给方法,因此有两行(注意参数)。

我发现在定义类的模块中使用Exporter 原则上很尴尬(但我不知道这样做有任何实际问题);它会导致一个潜在的混乱界面。这本身就是分离接口的一个很好的理由,以便功能性的必须加载特定的模块。

还有一个细节需要注意。方法调用

($obj_cache || __PACKAGE__->new)->f1(...)

使用缓存的$obj_cache(如果这个子已经被调用)来进行调用。因此,对象的状态被保留,在之前对f1 的调用中可能已经或没有被操纵。

这在用于非面向对象的上下文中的调用中相当重要,应该仔细研究。如果有问题,只需删除该缓存或将其扩展为完整的 if 语句,其中可以根据需要重置状态。

这两种用途绝对不能在同一个程序中混用。


要使用派生类进行测试,我使用最小的

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

额外的输出是

作为派生类的方法调用 f1中的args:NextDupl=HASH(0x11ac720),作为派生类的方法调用 通过派生类从父级提取的coderef调用 f1 中的 args:NextDupl,由派生类通过父级的 coderef 调用

这包含使用UNIVERSAL::can 的测试,因为它出现在评论中。


在 cmets 中提出并讨论了一个特定的限制(我知道)。

假设我们编写了一个方法,它接受一个对象(或一个类名)作为它的第一个参数,所以被调用为->func($obj);进一步——这很重要——这个方法允许任何类,因为它的工作方式不关心它有什么类。这将是非常特殊的,但这是可能的,并且会引发以下问题。

这个方法对应的函数调用会是func($obj),而当$obj恰好在这个类的层次结构中会导致方法调用->func()不正确

在决定是否 它被称为函数或方法,因为它所做的只是查看第一个参数。如果它是我们自己的层次结构中的对象/类,它会确定这是对该对象的方法调用(或类方法调用),在这种特殊情况下是错误的。

模块的作者有两种简单的方法,可能还有另一种方法来解决这个问题

  • 不为这种高度特定的方法提供功能接口

  • 给它一个单独的(明确相关的)名称

  • 通过检查第一个参数来决定如何调用我们的if 条件是固定的,但仍然为每个具有该接口的方法编写。所以在这个方法中再检查一个参数:如果第一个是这个类的对象/类并且下一个是(任何)对象/类,那么它是一个方法调用。 如果第二个参数是可选的,这不起作用。

这一切都是完全合理的。在一个行使其定义特征、拥有和使用数据(“属性”)的类中,可能会有无法转换为函数调用的方法。这是因为单个程序应该只使用 one 接口,并且函数没有状态,因此依赖它的方法不会运行。 (为此使用缓存对象非常危险。)

因此,人们总是必须仔细决定界面,并进行选择。

感谢 Grinnz 提供 cmets。


请注意,“函数式编程”有一个完全不同的范式,而标题有点不清楚。所有这些都是关于过程方法中的功能接口。

【讨论】:

  • 您的“方法检测”有两个问题(不幸的 CGI.pm 代码共享):它不尊重继承,因此它会中断子类;如果您在将对象作为函数调用时将对象作为第一个参数传递,它将假定您将其作为方法调用,即使它类似于通常用作字符串的 Path::Tiny 对象。
  • @Grinnz 嗯,我的想法正好相反(所以排除了对 isa 的调用 --- 见文字)......让我检查......
  • 不仅是对isa的调用,而且您还需要使用isa而不是eq来检查它是否是一个类名,而不是一个引用,以便允许该类子类上的方法调用(例如Subclass->f1)。它还需要返回 $self 的实例或 $self 被祝福的类,而不是 __PACKAGE__。所有这些工作,当你做像my $sub = Class->can('f1'); OtherClass->$sub这样完全有效的事情时,它仍然会中断。
  • @Grinnz 好吧,不,当使用错误的类的对象调用时,它会被重定向到正常的方法解析,就像在任何直接类中一样。也许我看的不太清楚?
  • @Grinnz 对类方法调用的评论当然是好的;还有更多工作要做。这是一个插图,但也许我把它推得太远了。将审查并添加进一步的评论...
猜你喜欢
  • 1970-01-01
  • 2017-01-15
  • 1970-01-01
  • 2020-05-24
  • 2021-03-29
  • 2011-09-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多