Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Perl6: variable number of arguments to function/subroutine

Tags:

raku

I want to be able to run a function with a variable number of parameters in Perl6, but after reading through https://docs.perl6.org/language/functions#Arguments I don't see how it can be done. I see numerous links for other languages, and a warning that "Questions with similar titles have frequently been downvoted" but I don't see this anywhere in the documentation or StackOverflow.

I want to do something like this:

some-test($v1, $v2)

or

some-test($v3)

but not have a separate function using multi for each

How can I construct a Perl6 subroutine which will accept a variable number of strings?

like image 470
con Avatar asked Feb 15 '19 15:02

con


3 Answers

TL;DR You're asking about variadic functions.1 Simple use is simple. Some P6 features, most notably positional and named arguments, and variadic destructuring, add some wrinkles. Also, see the other answers which are very helpful too.

variable number of arguments

Simple use of a simple variadic function:

sub variadic (|args) { say args .elems }
variadic();           # 0
variadic('1');        # 1
variadic('1', '2');   # 2

A |foo parameter slurps up all remaining arguments into a Capture bound to sigilless identifier foo:

sub variadic ($a, @b, %c, |others) { say others[0] }
variadic 1, [2,3], (:foo), 4, [5,6], (:bar); # 4

In the above example the others parameter "slurps"1 up the last three listed arguments starting with the 4.

Variadic variations

Things aren't always simple:

variadic(1, 2);       # 2 -- works but args are Ints
variadic(:foo,:bar);  # 0 -- where did :foo, :bar go?
variadic((:foo));     # 1 -- works; arg is Pair (:foo)
variadic((1, 2));     # 1 -- works; arg is List (1,2)

In the rest of this answer I explain:

  • Constraining arguments, eg ensuring they're all strings.

  • Positional vs named arguments; **@foo and *%foo

  • Variadic positionals destructuring; +@foo and *@foo

Constraining arguments

variable number of strings

You can impose any constraints you want on slurped arguments by using a where clause. (Which you can in turn package into a subset if you want.)

For example, to constrain all the arguments to be of type Str:

sub variadic (|args where .all ~~ Str) { say args .elems }
variadic();         # 0
variadic('1');      # 1
variadic('1', '2'); # 2
variadic(1);        # Constraint type check failed

Positional vs named arguments; **@foo and *%foo

P6 supports both positional and named arguments.

Using |foo in a signature always captures all remaining arguments, both positional and named:

sub slurps-into-WHAT (|args) { say WHAT args }
slurps-into-WHAT(); # (Capture)

A Capture stores positional arguments in an internal list accessible via positional subscripting (i.e. args[...]) and named arguments in a hash accessible via associative subscripting (i.e. args<...> or args{...}):

sub variadic (|args) { say " {args[1]} {args<named2>}" }
variadic('pos0', 'pos1', named1 => 42, named2 => 99); # pos1 99

Sometimes it's preferable to collect just named args, or just positional ones, or to collect both but in separate parameters.

To collect named args, use a parameter of the form *%foo (one asterisk prefix and a hash arg):

sub variadic-for-nameds (*%nameds) { say %nameds }
variadic-for-nameds(:foo, :bar); # {bar => True, foo => True}

(Note that all methods collect named args in this way even if their signature doesn't explicitly say so.2)

To collect positional args, use a parameter of the form **@foo (two asterisk prefix immediately followed by an array arg):

sub variadic-for-positionals (**@positionals) { say @positionals }
variadic-for-positionals(1, 2, 3); # [1 2 3]

Variadic positionals destructuring; +@foo and *@foo

P6 provides a range of non-variadic argument destructuring features.

The first variadic positionals destructuring parameter form is +@foo. This has exactly the same effect as **@foo except in one case; if the variadic parameter gets just a single argument, and that argument is a list or array, then the parameter is bound to the content of that list or array, stripping away the list/array container:

sub variadic-plus (+@positionals) { say @positionals }
variadic-plus(1,2,3);   # says same as for **@positionals
variadic-plus(1);       # says same as for **@positionals
variadic-plus([1]);     # [1]     -- instead of [[1]]
variadic-plus((1,2,3)); # [1 2 3] -- instead of [(1 2 3)]

The +@foo form was introduced to support the "single arg rule". It's used by core devs writing built ins. Users may wish to use it when they want the same behavior.

The other variadic positionals destructuring form is *@foo. It does the same thing as +@foo in that it extracts the content from list or array container args and throws the container away. But it's much more aggressive:

  • It does this for all arguments.

  • If an argument is a list rather than an array ((...) rather than [...]) then it descends into that list and recursively repeats the exercise if an element of the list is itself another inner list or array.

Thus:

sub variadic-star (*@positionals) { say @positionals }
variadic-star((1,2),[3,4]);             # [1 2 3 4]
variadic-star((1,2),(3,4,(5,6,(7,8)))); # [1 2 3 4 5 6 7 8]
variadic-star((1,2),(3,4,[5,6,(7,8)])); # [1 2 3 4 5 6 (7 8)]

(Note how it stripped the container from the [5,6,(7,8)] array but did not descend into it.)

One final thing; are there variadic named destructuring parameters? You tell me.3

Bonus section: foo(...) vs foo (...)

(I included this bonus section in the hope it heads off confusion. If this section is itself confusing, just ignore it.)

Routine calls can be written with or without parentheses around their list of arguments and they mean the same thing. The opening parenthesis must follow the routine name immediately, without intervening whitespace:

sub foo  (|args)  { say args[0] }
foo 'a', 'b';    # a
foo('a', 'b');   # a

(This rule only applies to the routine call, between the routine name and its arguments. It does not apply to the routine declaration, between the routine name and its parameters. For the latter, the declaration, you can leave no space or use space as I have above with sub foo (|args) and it makes no difference.)

If you insert whitespace between the foo and the opening parenthesis in a call you're writing something different:

foo  ('a', 'b'); # (a b)

That calls foo with one argument, the one list ('a', 'b') in contrast to foo('a', 'b') which calls foo with two arguments, the two values inside the parentheses.

The following calls foo with two arguments, both of which are lists:

foo ('a', 'b', 'c'),  ('d', 'e', 'f'); # (a b c)

You may nest parentheses:

foo(  ('a', 'b', 'c'),  ('d', 'e', 'f')  )    ; # (a b c)
foo  (('a', 'b', 'c'),  ('d', 'e', 'f'))      ; # ((a b c) (d e f))
foo( (('a', 'b', 'c'),  ('d', 'e', 'f')) )    ; # ((a b c) (d e f))

The latter two foo calls get one argument, the one list ( ('a', 'b', 'c'), ('d', 'e', 'f') ) (that happens to contain two inner lists).

Footnotes

1 The standard industry term for this is a variadic function. At the time of writing this answer, the Rakudo P6 compiler uses the industry standard term ("variadic") in error messages but the official P6 doc tends to use the word "slurpy" instead of "variadic" and talks of "slurping up arguments".

2 Methods always have an implicit variadic named parameter called %_ if one is not explicitly specified:

say .signature given my method foo {} # (Mu: *%_)

3 The P6 language and/or the Rakudo P6 compiler currently allows parameters to be written in the form +%foo and **%foo. It doesn't really make sense to have variadic nameds destructuring. Perhaps that explains why both these forms do insane things:

  • **%foo appears to be indistinguishable from %foo.

  • +%foo appears to be indistinguishable from **@foo except for using the identifier %foo instead of @foo. The object bound to %foo is an Array!

like image 196
raiph Avatar answered Nov 20 '22 12:11

raiph


If you just want to be able to take 1 or 2 values then the easiest way is to mark the second value as optional :

sub some-test( $v1, $v2? ) { ... }

Or you can define a default value :

sub some-test( $v1, $v2="default" ) { ... }

Or if you want any number of values (1 or more) you can use a slurpy with a where clause :

sub some-test( *@v where *.elems > 0 ) { ... }
like image 7
Scimon Proctor Avatar answered Nov 20 '22 11:11

Scimon Proctor


You can use signature destructuring

sub some-test(*@all [$first, *@rest]) { ... } # must have 1 or more parameters
like image 7
ugexe Avatar answered Nov 20 '22 10:11

ugexe