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.
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);
...
}
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.
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"
]