jo37 has asked for the wisdom of the Perl Monks concerning the following question:
Dear Nuns and Monks,
after some digging into subroutine signatures I stumbled upon a little problem that seems to be non-existing without signatures:
How to distinguish between a missing and an undefined argument?
Without signatures it goes straight forward: shift arguments as long as there are any left over.
There is no problem distinguishing between non-existing and undefined.
See no_sig in the example below.
With signatures, a naive approach would be to check the size of @_, too.
This appears to be some kind of illogical to me, see sig_std in the example.
Going one step further, I came to a lexical variable defined inside the signature that is set only in case of a missing argument.
This apparently works but I'm not sure if this is indeed valid.
The argument and the new variable are named alike to establish a connection between them.
See sig_lexical in the example.
Is there a common idiom how to handle such case with signatures?
I'm not satisfied with my approaches in the example.
Any other ideas?
#!/usr/bin/perl
use v5.12;
use Test2::V0;
use experimental 'signatures';
sub show ($arg) {
defined $arg ? $arg : 'undef';
}
# No signature, no check.
sub no_sig {
my $what = shift;
state $data;
# Straight forward: shift off first arg if there is any.
if (@_) {
my $arg = shift;
say "$what: ", show $arg;
$data = $arg;
} else {
say "$what: ", show $data;
return $data;
}
}
# Standard signature with optional second arg.
sub sig_std ($what, $arg=undef) {
state $data;
# Kind of illogical: there is no obvious connection between @_, it
+s
# size and $arg.
if (@_ >= 2) {
say "$what: ", show $arg;
$data = $arg;
} else {
say "$what: ", show $data;
return $data;
}
}
# Signature with side effect on missing arg.
sub sig_lexical ($what, $arg=(my $no_arg=1, undef)) {
state $data;
# $arg and $no_arg are (loosely) connected by their names. Howeve
+r,
# there seems to be no specification about lexical variables
# declared within a signature.
if (!$no_arg) {
say "$what: ", show $arg;
$data = $arg;
} else {
say "$what: ", show $data;
return $data;
}
}
pass 'init';
no strict 'refs';
for my $sub (qw(no_sig sig_std sig_lexical)) {
# Set to 1 and retrieve:
&$sub("$sub setter", 1);
is &$sub("$sub getter"), 1, "$sub get 1";
# Set to undef and retrieve:
&$sub("$sub setter", undef);
is &$sub("$sub getter"), U(), "$sub get undef";
SKIP:
{
skip "arg check without signature" if $sub =~ /^no/;
# Too many arguments:
like dies {&$sub("$sub invalid args", 42, 1)},
qr/Too many arguments/,
"$sub with three args";
}
}
done_testing;
__DATA__
# Seeded srand with seed '20201227' from local date.
ok 1 - init
no_sig setter: 1
no_sig getter: 1
ok 2 - no_sig get 1
no_sig setter: undef
no_sig getter: undef
ok 3 - no_sig get undef
ok 4 - skipped test # skip arg check without signature
sig_std setter: 1
sig_std getter: 1
ok 5 - sig_std get 1
sig_std setter: undef
sig_std getter: undef
ok 6 - sig_std get undef
ok 7 - sig_std with three args
sig_lexical setter: 1
sig_lexical getter: 1
ok 8 - sig_lexical get 1
sig_lexical setter: undef
sig_lexical getter: undef
ok 9 - sig_lexical get undef
ok 10 - sig_lexical with three args
1..10
Greetings, -jo
$gryYup$d0ylprbpriprrYpkJl2xyl~rzg??P~5lp2hyl0p$
Re: Distinguish between missing and undefined arguments with subroutine signatures
by choroba (Archbishop) on Dec 27, 2020 at 17:26 UTC
|
There is nothing like a missing argument with signatures:
#!/usr/bin/perl
use warnings;
use strict;
use experimental 'signatures';
use Syntax::Construct qw{ // };
sub show ($arg) { $arg // 'undef' }
use Test::More;
use Test::Exception;
throws_ok { show() } qr/Too few arguments/;
done_testing();
map{substr$_->[0],$_->[1]||0,1}[\*||{},3],[[]],[ref qr-1,-,-1],[{}],[sub{}^*ARGV,3]
| [reply] [d/l] [select] |
|
| [reply] |
|
Oh, sorry, I didn't get it.
Why do you need it?
It seems you don't want an optional argument with a default value, you want a subroutine that knows whether setting the default value was triggered. It's hard to do it with just the optional argument, as it's a single value, but you're interested in two values: the value itself and the boolean telling whether the default value was triggered.
Both your solutions introduce such a value (whether it's the scalar @_ or the additional lexical variable). I'm not sure which one I'd prefer, but I'd probably add a comment to both of them.
map{substr$_->[0],$_->[1]||0,1}[\*||{},3],[[]],[ref qr-1,-,-1],[{}],[sub{}^*ARGV,3]
| [reply] [d/l] |
Re: Distinguish between missing and undefined arguments with subroutine signatures
by LanX (Sage) on Dec 27, 2020 at 18:52 UTC
|
> Is there a common idiom how to handle such case with signatures?
The rule of thumb is to use a default which is defined. °
Using undef as default should be redundant and will force you to check @_ - if still possible (untested)
Sorry, your code is longish (aka TLDR), if I didn't get it right, please consider condensing it to the relevant part.
This question could be actually very relevant if there is a real use case! P5P is considering to drop the population of @_ when signatures are used.
update
°) not
# Standard signature with optional second arg.
sub sig_std ($what, $arg=undef) {
| [reply] [d/l] [select] |
|
P5P is considering to drop the population of @_ when signatures are used
But only after alternative facilities for argument instrospection have been provided. See the
proposal on the p5p mailing list.
In particular, in
sub foo($x, ??$has_y, $y = 0) { ... }
$has_y is true if and only if a second arg is passed to the sub.
Dave. | [reply] [d/l] |
|
| [reply] [d/l] |
|
The use case I have in mind is a combined getter/setter for a perl object.
Return the value when called without an additional arg, set the value when called with an arg.
A call with an undefined arg clears the attribute - just like in my example.
Maybe that's not a real world use case - I'm just playing with things.
Greetings, -jo
$gryYup$d0ylprbpriprrYpkJl2xyl~rzg??P~5lp2hyl0p$
| [reply] |
|
> combined getter/setter ... Return the value when called without an additional arg,
from my experience are mutators written in a way to always return the value.
> A call with an undefined arg clears the attribute - just like in my example.
OK ... but in that case why do you have a default value at all?
Anyway, if you want to allow to set to undef and don't wanna introspect @_ , then you're stuck in a kind of semipredicate problem.
Such problems can be solved in Perl by setting a new "impossible"° object MISSING as default, like
use constant MISSING => bless {}, "MyPackg::__Missing__"
and later
sub mutator ($value = MISSING) { ...BODY... }
(untested)
If you see that an arguments equals to that "impossible" value blessed into your own private namespace below "MyPackg", you can be sure that it wasn't used by accident.
HTH! :)
update
clearer rewording, clearer code
°) of course it's possible that such a value is used, but extremely unlikely without intend.
| [reply] [d/l] [select] |
|
|
|
|
|
Re: Distinguish between missing and undefined arguments with subroutine signatures
by jo37 (Hermit) on Jan 07, 2021 at 15:29 UTC
|
# Unique catcher for a missing argument
use constant MISSING => bless {}, __PACKAGE__ . '::__missing__';
# Helper sub, that either checks its argument against the catcher
# or otherwise returns the catcher itself.
sub missing {
@_ ? $_[0] && $_[0] eq MISSING : MISSING;
}
# To be used with signatures in various places:
sub foo ($self, $val=missing) {
if (missing $val) {
# getter
} else {
# setter
}
}
Thanks, Rolf!
Greetings, -jo
$gryYup$d0ylprbpriprrYpkJl2xyl~rzg??P~5lp2hyl0p$
| [reply] [d/l] |
|
Your Welcome! :)
But I suppose you don't want the sub missing() to be available as method the way ->foo() is?
And while the MISSING object is a nice workaround for the semi-predicate problem, I still think it's an overkill here.
Like I said you could just slurp into @val and have the full functionality, with less code:
use strict;
use warnings;
use experimental "signatures";
use Carp;
{
package BLA;
sub new ($class,@properties) {
return bless {@properties}, $class;
}
sub foo ($self, @val) {
unless (@val){ # getter
return $self->{foo}
} elsif (@val == 1) { # setter
return $self->{foo} = $val[0];
} else { # ERROR
local $" = ",";
Carp::croak "Too many arguments for method 'BLA->foo(@val)
+'";
}
}
}
my $x = BLA->new(foo=>42);
warn "getter:", $x->foo;
warn "setter:", $x->foo(666);
warn "getter:", $x->foo;
warn "error:", $x->foo(42,666);
-*- mode: compilation; default-directory: "d:/tmp/pm/" -*-
Compilation started at Fri Jan 8 21:39:32
C:/Perl_524/bin\perl.exe -w d:/tmp/pm/missing_param.pl
getter:42 at d:/tmp/pm/missing_param.pl line 31.
setter:666 at d:/tmp/pm/missing_param.pl line 32.
getter:666 at d:/tmp/pm/missing_param.pl line 33.
Too many arguments for method 'BLA->foo(42,666)' at d:/tmp/pm/missing_
+param.pl line 34.
Compilation exited abnormally with code 255 at Fri Jan 8 21:39:33
updates
improved code with croak and better error message | [reply] [d/l] [select] |
|
But I suppose you don't want the sub missing() to be available as method the way ->foo() is?
That doesn't harm at all.
Calling $obj->missing just returns false, which is correct :-)
However, your objection discovered a flaw in my approach.
The missing sub needs to be prototyped to go one step further: multiple optional arguments.
And then slurping starts hurting :-)
This leads to:
#!/usr/bin/perl
use v5.16;
use warnings FATAL => 'all';
package Foo;
use experimental 'signatures';
use constant MISSING => bless {}, __PACKAGE__ . '::__missing__';
sub missing :prototype(;$) {
@_ ? $_[0] && $_[0] eq MISSING : MISSING;
}
sub new ($class) {
bless {}, $class;
}
sub foo ($self, $foo=missing, $bar=missing) {
say "foo is missing" if missing $foo;
say "bar is missing" if missing $bar;
}
package main;
my $foo = Foo->new;
say "none:";
$foo->foo;
say "one:";
$foo->foo(1);
say "two:";
$foo->foo(1, 2);
print "\n";
say 'object foo is not missing' unless $foo->missing;
__DATA__
none:
foo is missing
bar is missing
one:
bar is missing
two:
object foo is not missing
Greetings, -jo
$gryYup$d0ylprbpriprrYpkJl2xyl~rzg??P~5lp2hyl0p$
| [reply] [d/l] [select] |
|
|
|
|
|