Front page | perl.perl5.porters |
Postings from November 2019
Named Parameters
From:
Dave Mitchell
Date:
November 28, 2019 17:02
Subject:
Named Parameters
Message ID:
20191128170142.GB3573@iabyn.com
=head2 Synopsis:
sub foo (
$pos1, # positional parameter; consumes 1 arg
$pos2, # positional parameter; consumes 1 arg
:$name1, # named parameter, consumes ("name1", value) arg pair
:$name2, # named parameter, consumes ("name2", value) arg pair
@rest, # consumes all unrecognised name/value pairs
) { ... }
This seems a popular feature request: give Perl 5 something similar to
Perl 6's named parameters:
sub foo(:$name1, :$name2) { .... }
foo(name2 => 200, name1 => 100);
The Perl 5 variant will have to be a bit different from Perl 6, in that
information about the signature is not available to the caller, i.e. the
perl parser has no concept of named arguments at the call site. So in
Perl 5, named arguments will in fact be just two arguments used as a
name/value pair, which are mapped to a single named parameter. In
particular, while these two are different in Perl 6:
foo( name => 100); # a single named arg, with value 100
foo('name' => 100); # two positional args, with values 'name' and 100
they will be the same in Perl 5. Note also that Perl 5 will not do
compile-time checking of the caller's args for valid names.
I propose a fairly straightforward implementation for Perl 5: that in the
presence of at least one named parameter in the signature, any arguments
following all positional arguments are treated as name/value pairs, and
the values of recognised names are bound to the corresponding named
parameter. Any unrecognised pairs are left for a slurpy array or hash to
consume (or to croak if no slurpy). An odd number of named args will
croak.
So for example, this:
sub foo($pos1, $pos2, :$named1, :$named2, %rest) {...}
is about equal to the following:
sub foo($pos1, $pos2, %rest) {
my $named1 = delete $rest{'named1'};
my $named2 = delete $rest{'named2'};
...;
}
but with proper default and error handling, and better performance.
Duplicate named arguments are allowed, the last value being used. This
allows the useful idiom of foo(%defaults, %options) to work.
The ordering of parameter types within a signature would be extended to be:
1) zero or more mandatory positional parameters, followed by
2) zero or more optional positional parameters, followed by
3) zero or more mandatory named parameters, followed by
4) zero or more optional named parameters, followed by
5) zero or one slurpy array or hash
Except that would be a compile-time error to have both (2) and (3).
(3) and (4) are new. Note that here isn't any semantic need for any
optional named parameters to always follow all mandatory named parameters,
but including that restriction doesn't prevent you doing anything (as far
as I can see), provides consistency with positional parameters, and
potentially allows better optimisations.
Because named arguments can be supplied by the caller in any order, there
are issues as to which order default expressions are evaluated. To put
things on a firm footing, I propose a conceptual pre-sorting of the
argument list, followed by processing of parameters in strict
left-to-right order. See also the "Query Parameters" thread for how this
sorting is important there too. (This sorting is just conceptual: the
actual implementation can do whatever is most convenient or efficient, as
long as it manifests the same visible behaviour.)
The sort uses the following rules:
a) We start with the original raw argument list, i.e. before any
default expressions have been run or added to the argument list.
b) All positional arguments (which always come first) are left as-is.
c) any remaining arguments are treated as key value pairs (with a croak
if not even).
d) The pairs are stable sorted into order of named parameters, but
with any unrecognised pairs left in their original order at the
end. Duplicate recognised names are de-duplicated, keeping the
right-most value; unrecognised names *aren't* de-duplicated.
For example, with
sub foo($p1, p2, :$n1, :$n2, :$n3, :$n4 = undef, @rest) { ... }
foo(1, 2, x => -1, n2 => -2, n1 => 11, y => 1002,
x => 1001, n3 => 13, n2 => 12)
the args list gets sorted to:
(1, 2, # positional args
n1 => 11, n2 => 12, n3 => 13, # sorted, de-duped named args
x => -1, y => 1002, x => 1001) # unrecognised named args
Then each parameter is processed in turn in left-to-right order, consuming
the next argument or argument pair as appropriate, or evaluating the default
expression if the argument is missing.
Note that for duplicate arguments, only the right-most value is likely to
evaluated; for example foo(name => $tied1, name => $tied2) would likely
only call (tied $tied2)->FETCH(), although we don't guarantee this.
By pre-sorting the argument list into parameter order, then binding
arguments to parameters in L-R order, we have reduced any ordering issues
down to the same complexity as we already had with positional arguments,
e.g. whether earlier parameter variables are available for use by later
default expressions and in what order default expressions are evaluated.
Whereas in Perl 6, named parameters are optional by default, I think that
for Perl 5, making them mandatory by default makes more sense, for
consistency with positional parameters. If not, then we would need a new
syntax (like p6's :$foo!) to indicate mandatory.
=head2 Named parameter syntax
In the above discussion I have used the Perl 6 :$foo syntax, but we needn't
necessarily use that. We could potentially use a different character; or
more radically, we could divide the signature into two sections separated
by a semicolon. For example
sub foo($p1, p2, :$n1, :$n2, @slurpy)
would instead be written as:
sub foo($p1, p2;
$n1, $n2, @slurpy)
I prefer the former. It's noisier, but conversely the noise makes the fact
that they're named parameters stand out.
Note that in neither case is the slurpy a named parameter: at no point
does the caller do 'foo(..., slurpy => ... )'. And in fact :@foo and :%bar
as parameters are compile-time errors. If you want a named list, use a
named reference alias instead, e.g.
sub foo(..., \:@coordinates);
foo(..., coordinates => [1,2,3]).
For the (...; ...) version, we would need to decide whether an empty named
parameter list is legal. Perhaps allow it, in case we want in future to
add more ';'-separated sections to a signature.
In terms of characters, the following are already taken, or might be taken
under some of the other proposals:
$ @ % sigils
\ * aliasing
? query parameter
, ) signature syntax
# comment
Personally I think we should stick with ':'.
I don't think the ':' should be considered part of the sigil, and
whitespace should be allowed, e.g. (: $n1, : $n2). Note that Perl 6
doesn't allow whitespace.