Seek Perl idiom to check that $self is a class or

2019-04-07 00:16发布

问题:

In Perl, I just got bitten by something that looked like the bug below:

package Foo;
sub method {
    my $self = shift;
    my @args = @_;
    ...
}

where I called it as a subroutine, not a method:

Foo::method( "arg1", "arg2" );

rather than calling it as a method - in this case, it was a "class method":

Foo->method( "arg1", "arg2" );

Calling Foo::method("arg1","arg2") resulted in "arg1" getting dropped.

Similar considerations can arise with an "object method":

my $object = Foo->new();
$obj->method( "arg1", "arg2" );

Is there a friendly, concise, Perl idiom for checking that the first argument, conventionally called $self, is in fact an object in the class (package), and/or the class/package name?

The best I have come up with is:

package Foo;
sub method {
    my $self = ($_[0]->isa(__PACKAGE__) ? shift @_ : die "...error message...";
    my @args = @_;
    ...
}

which is not much more concise than

package Foo;
sub method {
    my $self = shift;
    die "...error message..."  if $self->isa(__PACKAGE__);
    my @args = @_;
    ...
}

or

package Foo;
use Carp::Assert;
sub method {
    my $self = shift;
    assert($self->isa(__PACKAGE__));
    my @args = @_;
    ...
}

Notes:

I know about Perl signatures, but dislike using experimental features.

I know about use attributes and :method. Is that the best way to go? Similar concerns about "evolving" features.

I know about Moose - but I don't think that Moose enforces this. (Did I miss anything.)

The problem with Perl is that there are so many ways to do something.

回答1:

The best answer is to not mix functions and methods in a single package. "Hybrid modules", as they're known, are problematic. Everything which you might want to make a function should instead be a class method call.

There should be little need to fully qualify a function call in day-to-day programming.


The most concise way is to use Moops which is the new way to use Moose with syntax-sugar.

use Moops;

class Foo {
    method something() {
        print("something called\n");
    }
}

Foo->new->something();
Foo::something();

# something called
# Invocant $self is required at /Users/schwern/tmp/test.plx line 10.

Moops is marked as unstable, but that's the interface, not the signatures themselves. Signatures have been around and usable in production for a long time, longer than they've been built in. More worrying is there hasn't been a release in over a year, however the author writes good stuff. Your call.


Otherwise, like with anything else, write a function.

use Carp;
use Scalar::Util qw(blessed);

sub check_invocant {
    my $thing = shift;

    my $caller = caller;

    if( !defined $thing ) {
        croak "The invocant is not defined";
    }
    elsif( !ref $thing ) {
        croak "The invocant is not a reference";
    }
    elsif( !blessed $thing ) {
        croak "The invocant is not an object";
    }
    elsif( !$thing->isa($caller) ) {
        croak "The invocant is not a subclass of $caller";
    }

    return $thing;
}

Since this returns the invocant and handles the exception for you it can be used very concisely.

package Foo;

sub method {
    my $self = ::check_invocant(shift);

    ...
}


回答2:

I'll add to what Schwern has written to say that you could also take a look at Safe::Isa, which lets you safely call isa on something which you cannot be sure is an object.



回答3:

I'm going to try to follow the advice of @Schwern and "not mix functions and methods in a single package". That said, here's an example using the fun method approach from Function::Parameters. The example is of course contrived and a bit awkward, but it illustrates the idea.

Function::Parameters requires a compiler version of at least perl5.14. It's still perl (and XS) so it will not magically make your code "strongly typed". But, with attributes and type constraints via Type::Tiny, you can separate your methods and functions by more than name only. Even just using different names for different types of subroutines - fun and method by default - can be really helpful.

Using the ':strict' keyword and/or default function/method "types" (fun => { ... } and method => { ... } below, as well as others such as method_lax) obviates the need for passing values to settings when the module is imported, so the code below can be made shorter.

use v5.22;   
package My::Package {
  use DDP;
  use attributes 'get';
  use Function::Parameters { 
    fun    =>  { strict => 1, } , 
    method =>  { strict => 1,
                 invocant => 1, 
                 shift => '$class', 
                 attributes => ':method',} ,  
  } ;

  fun func_test ( @ )  {
   warn "must be called as a function" 
   if $_[0] eq __PACKAGE__ && get(__SUB__) ne "method";       
   print "args = ", np @_ ;
  } 

  method meth_test ( @ ) { 
   warn "must be called as a method" 
   unless $class eq __PACKAGE__ && get(__SUB__) eq "method";  
   say "\$class = $class" if length $class ;
   say "args = ", np @_  ;
  }
}

say "\nCalling meth_test as method:";
My::Package->meth_test( ["foo", "bar"] );    
say "\nCalling meth_test as function:";
My::Package::meth_test( ["foo", "bar"] );
say "\nCalling func_test as a function:";
My::Package::func_test( qw/baz fuz/ );
say "\nCalling func_test as a method:";
My::Package->func_test( qw/baz fuz/ );

Output:

Calling meth_test as method:
$class = My::Package
args = [
    [0] [
        [0] "foo",
        [1] "bar"
    ]
]

Calling meth_test as function:
must be called as a method at FunctionParameters-PackageCheck-SO.pl line 24.
$class = ARRAY(0x801cfa330)
args = []

Calling func_test as a function:
args = [
    [0] "baz",
    [1] "fuz"
]

Calling func_test as a method:
must be called as a function at FunctionParameters-PackageCheck-SO.pl line 17.
args = [
    [0] "My::Package",
    [1] "baz",
    [2] "fuz"
]


标签: perl oop