Day 9: Having beautiful arguments (and parameters)

by

On the ninth day of advent, we unwrap… the syntax for parameters and arguments.

You may or may not be familiar with the way Perl 5 does parameter handling in subroutines. It goes something like this:

  sub sum {
    [+] @_
  }
  say sum 100, 20, 3;  # 123

The [+] is Perl 6, but we might as well have written that as my $i = 0; $i += $_ for @_; $i, and the above would have worked in Perl 5. The important point is that, in Perl 6 as well as in Perl 5, when you call a subroutine, your parameters can be found in @_. You unpack them, and you do your thing.

This system is extremely flexible, because it doesn’t impose any mechanism on the handling of parameters; it’s all up to you, the programmer. It’s also tedious, and favours boilerplate to some extent. Take this fictional example:

  sub grade_essay {
    my ($essay, $grade) = @_;
    die 'The first argument must be of type Essay'
      unless $essay ~~ Essay;
    die 'The second argument must be an integer between 0 and 5'
      unless $grade ~~ Int && $grade ~~ 0..5;

    %grades{$essay} = $grade;
  }

(You’d have to use isa instead of ~~ and $grades instead of %grades to make this work in Perl 5, but apart from that, this code would work in Perl 5 as well as in Perl 6.)

Now, for a moment, look at the above, and despair about the amount of manual unpacking and verification you have to make. Feel it? Good.

Perl 5 solves this by giving you excellent CPAN modules, such as Sub::Signatures and MooseX::Declare, for example. Just include them, and you’re set.

Perl 6 solves it by giving you a few more defaults. By “a few more”, I really mean “make sure you don’t drool on your keyboard”. In Perl 6, I would have written the above routine like this:

  sub grade_essay(Essay $essay, Int $grade where 0..5) {
    %grades{$essay} = $grade;
  }

Now we’re talking. It has the same runtime semantics as the longer version above. And I didn’t have to import any CPAN modules, either.

Sometimes it’s convenient to provide default values to parameters:

  sub entreat($message = 'Pretty please, with sugar on top!', $times = 1) {
    say $message for ^$times;
  }

The default values need not be constants, and they can in fact use earlier parameters:

  sub xml_tag ($tag, $endtag = matching_tag($tag) ) {...}

If your default is unspecified, just mark the parameter as optional without giving it a default, by using ?:

  sub deactivate(PowerPlant $plant, Str $comment?) {
    $plant.initiate_shutdown_sequence();
    say $comment if $comment;
  }

One feature I particularly like is that you can refer to the parameters by name when calling, passing the named arguments in any order you like. I can never remember the parameter order in functions like this:

  sub draw_line($x1, $y1, $x2, $y2) { ... }

  draw_line($x1, $y1, $x2, $y2);  # phew. got it right this time.
  draw_line($x1, $x2, $y1, $y2);  # dang! :-/

There’s a way to refer to the parameters by name, which makes this problem melt away:

  draw_line(:x1($x1), :y1($y1), :x2($x2), :y2($y2));  # works
  draw_line(:x1($x1), :x2($x2), :y1($y1), :y2($y2));  # also works!

The colon means “here comes a named argument”, and the whole construct is to be read as :name_of_parameter($variable_passed_in). There’s a short form that can be used when the parameter and the variable happen to have the same name, though:

  draw_line(:$x1, :$y1, :$x2, :$y2);  # works
  draw_line(:$x1, :$x2, :$y1, :$y2);  # also works!

I like the short form. I find it causes my code to be more readable.

If you, as the API author, want to force people to use named arguments — which might actually be appropriate in the case of draw_line — you need only supply the colons in the subroutine signature.

  sub draw_line(:$x1,  :$y1,  :$x2,  :$y2 ) { ... }  # optional nameds

But be careful, named arguments are optional by default! In other words, the above is equivalent to this:

  sub draw_line(:$x1?, :$y1?, :$x2?, :$y2?) { ... }  # optional nameds

If you want to explicitly make parameters required, you can append a ! to them:

  sub draw_line(:$x1!, :$y1!, :$x2!, :$y2!) { ... }  # required nameds

Now the caller has to pass them, just as with ordinary positional parameters.

What about varargs? Say you want an arbitrary number of arguments passed in. No problem: just make the parameter an array and precede it with a *:

  sub sum(*@terms) {
    [+] @terms
  }
  say sum 100, 20, 3;  # 123

I use the same example as we started out with to make a point: when you don’t supply a signature to your subroutine, what you end up with is the signature *@_. This is the signature that emulates Perl 5 behaviour.

But an array with a * in front of it (a ‘slurpy array’) only captures positional arguments. If you want to capture named arguments, you’d use a ‘slurpy hash’:

  sub detect_nonfoos(:$foo!, *%nonfoos) {
    say "Besides 'foo', you passed in ", %nonfoos.keys.fmt("'%s'", ', ');
  }

  detect_nonfoos(:foo(1), :bar(2), :baz(3));
       # Besides 'foo', you passed in 'bar', 'baz'

Oh, and this might be a good time to mention that you can also pass in named parameters with a more hash-like syntax, like so:

  detect_nonfoos(foo => 1, bar => 2, baz => 3);
       # Besides 'foo', you passed in 'bar', 'baz'

Here’s one important difference to Perl 5: parameters are readonly by default:

  sub increase_by_one($n) {
    ++$n
  }

  my $value = 5;
  increase_by_one($value);  # boom

There are two chief reasons for making parameters readonly; one being efficiency. Optimizers like when variables are read-only. The other reason has to do with encouraging the right habits in the programmer, and make it slightly more cumbersome to be sloppy. Functional programming is good not only for the optimizer, but for the soul as well.

Here’s what you need to do to make the above work:

  sub increase_by_one($n is rw) {
    ++$n
  }

  my $value = 5;
  say increase_by_one($value);  # 6

Sometimes is rw is what you want, but sometimes you’d rather be modifying a copy of whatever was sent in. That’s when you use is copy:

  sub format_name($first, $middle is copy, $last) {
    $middle .= substr(0, 1);
    "$first $middle. $last"
  }

The original will be left unchanged.

In Perl 6, when you pass in an array or a hash, it doesn’t flatten out over several arguments by default. Instead, you have to use the | to make it flatten.

  sub list_names($x, $y, $z) {
    "$x, $y and $z"
  }

  my @ducklings = <huey dewey louie>;
  try {
    list_names(@ducklings);
  }
  say $!;                       # 'Not enough positional parameters passed;
                                #  got 1 but expected 3'
  say list_names(|@ducklings);  # 'huey, dewey and louie'

Similarly, if you flatten a hash, its contents will be sent in as named arguments to the routine.

Just as you can pass in arrays and hashes, you can also pass in code blocks:

  sub traverse_inorder(TreeNode $n, &action) {
    traverse_inorder($n.left, &action) if $n.left;
    action($n);
    traverse_inorder($n.right, &action) if $n.right;
  }

The three sigils are really type constraints:

  @  Array    (actually, Positional)
  %  Hash     (actually, Associative)
  &  Code     (actually, Callable)

…with the $ sigil working as the unconstrained version.

Watch out! An easy trap that people fall into is specifying the type constraint twice, both through a type and through a sigil:

  sub f(Array @a) { ... }  # WRONG, unless you mean Array of Array
  sub f(      @a) { ... }  # probably what you meant
  sub f(Int   @a) { ... }  # Array of Int

If you followed through to this point, you deserve another Perl 6 one-liner.

  $ perl6 -e '.fmt("%b").trans("01" => " #").say for <734043054508967647390469416144647854399310>.comb(/.**7/)'
  ###           ##   ###
  # #  ##  # ##  #  #
  ### #  # ##    #  ####
  #   #### #     #  #   #
  #   #    #     #  #   #
  #    ##  #     ##  ###
About these ads

11 Responses to “Day 9: Having beautiful arguments (and parameters)”

  1. pht Says:

    you mention

    sub increase_by_one($n is rw) {
    ++$n
    }

    but it is worth mentioning that this also serves another purpose, as it SHOULD croak on increase_by_one(42). ie. the rw should constrain the parameter to non-constant things. sadly rakudo still fails this one (or at least last time i checked).

    • Moritz Says:

      There’s a branch in Rakudo where lots of things are reworked (the ‘ng’ branch, if anybody is curious). On that this piece of code faithfully reports Cannot assign to readonly value.

      We expect that branch to land some time this month, or at the very latest due to the release in January.

  2. ? Says:

    If you’re a Perl 5 user, and feel left out looking at the last one-liner, here’s something roughly equivalent:

    print(($_ = (sprintf(“%b”, $_))) =~ tr/01/ #/ && $_, “\n”) for grep { $_ ne “” } split /(\d{7})/, “734044754508967647390469414544647854399310″;

    I won’t admit it’s the best way to get it in Perl 5 as a one-liner…

    • Moritz Says:

      Instead of split and grep you can also use "73..." =~ /(.{7})/g, to stay closer to the Perl 6 version :-)

      • ? Says:

        This is probably much better:

        $_ = sprintf(“%b”, $_) and tr/01/ #/ and print $_, “\n” for “734044754508967647390469414544647854399310″ =~ /(.{7})/g;

        I’m looking forward to having chained operations so much nicer in Perl 6.

  3. bened Says:

    $middle .= substr(0, 1);
    -> Started reading this like Perl 5 before I realized the new meaning. : )

    I’m really impressed with the short form of named parameters. I think no other language could possibly do this right now. (Well, maybe you could do it in Common Lisp with macros.)

    • carl Says:

      The short form of named arguments is really, really nice. Not least because it interacts seamlessly with twigils (by ignoring them) — something that only becomes a desired feature after a while, when you suddnely think “hm, will it work if I do this? It cannot possibly work if I do this…” …and it does!

      foo(:$*global, :$^placeholder, :$!attr, :$.accessor);

  4. Daniel Brockman Says:

    A similar short form for named parameters exists in O’Caml:

    To pass 123 as the value of the named parameter “foo”, you use this syntax:

    ~foo: 123

    But if you have 123 stored in a variable called “foo”, you can simply do this:

    ~foo

    It’s very, very convenient, and it also all but forces you to name your variables and parameters thoughtfully and consistently. Good move to include this feature in Perl 6.

    • carl Says:

      I think the point where I really fell in love with the short form of named parameters was when I realized that they can be used even with variables that have a twigil (secondary sigil) — for the purposes of the parameter naming, it’s simply ignored. So these examples all work:

      sub outer { inner(:$^some-arg); } # :some-arg($^some-arg)

      configure-io(:$*IN); # :IN($*IN)

      method build() { precompute-stuff(:$!a, :$!b, :$!c); } # :a($!a) etc.

  5. szeryf Says:

    Why isn’t the action parameter used in recursive calls to traverse_inorder? Is the code-parameter special (as in Ruby)? Or (if it’s normal) can we use more than one code-parameter (unlike in Ruby)?

    • carl Says:

      Oops, that was a thinko on my part — the &action does need to be specified. Now corrected in the post. Thanks.

      A few random comments: a Perl 6 compiler with the appropriate presence of mind would flag such an omission at compile time. (There’s no such compiler yet, but it’s definitely possible.) Regardless, the code wouldn’t make it through runtime since the signature binding would fail.

      I was once sitting at a dinner table opposite Damian Conway and Larry Wall. Larry was pondering adding a special code-parameter, as in Ruby, with a special syntactic exception so that omission of a comma for a block specified inline would not be an error, as it is now in Perl 6. That idea was just idle speculation, though, and hasn’t made it into spec.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s


Follow

Get every new post delivered to your Inbox.

Join 37 other followers

%d bloggers like this: