Author Archive

Day 9: Having beautiful arguments (and parameters)

December 9, 2009

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/)'
  ###           ##   ###
  # #  ##  # ##  #  #
  ### #  # ##    #  ####
  #   #### #     #  #   #
  #   #    #     #  #   #
  #    ##  #     ##  ###

Day 2: The beauty of formatting

December 2, 2009

Unwrapping the second gift brought to you by Perl 6 this Advent, we find… a method named .fmt.

If you’re familiar with sprintf, you’ll feel right at home with .fmt. If you haven’t heard about sprintf before, or if you’ve heard of it but are a bit fuzzy on the details, you might want to skim the perldoc page. Don’t drown in it, though; it’s longish. Just savour it.

Back to .fmt, sprintf‘s spunky little sister. Here are a few ways to use .fmt to format strings and integers.

  say 42.fmt('%+d')                # '+42'
  say 42.fmt('%4d')                # '  42'
  say 42.fmt('%04d')               # '0042'
  say :16<1337f00d>.fmt('%X')      # '1337F00D'

All this is good and well, but not really more than a shorter method form of sprintf. Big deal, right?

What I haven’t told you yet is that .fmt is overloaded, and works differently on arrays (or more precisely, lists):

  say <huey dewey louie>.fmt       # 'huey dewey louie'
  say <10 11 12>.fmt('%x')         # 'a b c'
  say <1 2 3>.fmt('%02d', '; ')    # '01; 02; 03'

Similarly, it’s overridden on hashes (or rather, mappings):

  say { foo => 1, bar => 2 }.fmt   # 'foo     1
                                   #  bar     2'
  say { Apples => 5, Oranges => 10 }.fmt('%s cost %d euros')
                                   # 'Apples cost 5 euros
                                   #  Oranges cost 10 euros'
  say { huey => 1, dewey => 2, louie => 3 }.fmt('%s', ' -- ')
                                   # 'huey -- dewey -- louie'

The way hashing works may give your output a different order than the ones shown above. Oh, and there’s an overloaded .fmt for pairs as well, but it works analogously to the one for hashes.

.fmt is a useful little tool to have when you want to change some value, or an array or a hash of values, into to some given format. It’s like sprintf, but tailored to Do What You Mean for arrays and hashes, too.

There’s only one risk in all of this: Perl 6 might soil the reputation of the Perl family of languages by simply being too darn readable. In order to counter this risk, I leave a small parting gift in the form of a simple-but-dense Christmas tree printing Perl 6 one liner:

  $ perl6 -e 'say " "x 9-$_,"#"x$_*2-1 for 0..9,2 xx 3'

          #
         ###
        #####
       #######
      #########
     ###########
    #############
   ###############
  #################
         ###
         ###
         ###
[*] If you are using Windows, remember you need to switch the quotes around
c:\>perl6.exe -e "say ' 'x 9-$_,'#'x$_*2-1 for 0..9,2 xx 3"

Follow

Get every new post delivered to your Inbox.

Join 37 other followers