Fun with code blocks in Perl 6

masak on 2008-12-20T18:34:25

Here's a little pattern I've discovered while hacking away at a board game implementation in Perl 6.

I had a subroutine called input_valid_move, whose job it was to read a move from $*IN, and return the move if it was valid according to the rules of the game. Easy enough.

repeat {
    print "\n", $player, ': ';
} until my $move = input_valid_move(...);

Now, there are several ways a move can be illegal, and I found myself printing and returning a lot from the sub:

unless $row_diff == 2 && $column_diff == 0
    || $row_diff == 0 && $column_diff == 2 {

    say 'Must be exactly two cells apart';
    return;
}

unless @heights[$row_1][$column_1]
    == @heights[$row_2][$column_2] {

    say 'Must be supported at both ends';
    return;
}

Notice the repetition? There were many (7) such tests for move correctness, and all of them made a boolean test, printed something and then returned from the sub:

if ( ... ) { # or 'unless'; depends
    say '...';
    return;
}

Repetition is a sign that there there is an abstraction just waiting to be created. I wanted to make an abstraction flunk_move that closed over the say '...'; return part of the above pattern, parametrizing the message printed. That way, I could just write this instead:

    flunk_move 'Must be exactly two cells apart'
        unless $row_diff == 2 && $column_diff == 0
            || $row_diff == 0 && $column_diff == 2;

    flunk_move 'Must be supported at both ends'
        unless @heights[$row_1][$column_1]
            == @heights[$row_2][$column_2];

Each move correctness test now became a single statement, instead of an if/unless statement containing two statements. As an added bonus, the most important part of the statement (the disqualification of the move) is now leftmost in the statement, something Damian Conway talks about in his book "Perl Best Practices".

But a new subroutine would not do as a repetition-reducing abstraction. The return statement in such a new sub, having moved from its original environment would be a no-op. I wanted to eat the cake and have it, too.

S06 states that the return function throws a control exception that is caught by the current lexically enclosing Routine, and this fact turned out to be just what I needed. To decipher the Perl 6 designese, the return in a sub returns from that sub, but the return in a bare block returns from the sub (or whatever) it was called from.

  # not what I want -- the return does nothing
  sub flunk_move($reason) { say $reason; return };

  # what I want, using pointy block
  -> $reason { say $reason; return };

  # what I want, using placeholder variables
  { say $^reason; return };

Think of it in biological terms: a sub is like a eukaryote: a little more complex, handles advanced things like return when necessary. A bare block doesn't have all that advanced piping, and has to delegate its return calls to its surrounding host cell. In other words, a bare block is a bit like an endosymbiont prokaryote, a simple organism that in the course of evolutionary history ended up in a symbiotic relationship inside a larger eukaryotic cell.

Biological analogies aside, what it meant to me was that I could do this in my sub input_valid_move:

my &flunk_move = { say $^reason; return };
(There's the endosymbiont, right there! It can't return from itself, because it's just a humble code block, so it returns from its surrounding subroutine instead, which happens to be input_valid_move.)

After that, I could use &flunk_move just as I wanted, as if it were a return statement with side effects. (Same code as above.)

    flunk_move 'Must be exactly two cells apart'
        unless $row_diff == 2 && $column_diff == 0
            || $row_diff == 0 && $column_diff == 2;

    flunk_move 'Must be supported at both ends'
        unless @heights[$row_1][$column_1]
            == @heights[$row_2][$column_2];

Some Smalltalk people extol the power in being able to define things like the if statement from within the language, without any magical trickery to make it work. The pattern I discovered above uses the same kind of strengths, the ability to define my own slightly fancy return statement, and have it look like a built-in in subsequent code.

That kind of power is what makes Perl 6 a joy to use.


"As an added bonus..."

Ron Savage on 2008-12-20T20:31:22

Hi. Where you talk about Damian's recommendation, I think you've got it backwards. On p 93 of his book he says 'avoid using the postfix form of if'. But that's exactly what you're doing!
Cheers

Re:"As an added bonus..."

chromatic on 2008-12-20T21:01:21

Page 94 says "Reserve postfix if for flow-of-control statements." It's his only exception to the previous guideline.

Re:"As an added bonus..."

masak on 2008-12-20T21:23:33

Right, and this is a fancy return, hence control flow. I forgot to clarify that.

Obscure

ChrisDolan on 2008-12-21T15:04:40

While clever, I think you've harmed readability rather than helped it by hiding the return in the block. As a reader, I want the flow control statements to be as explicit and obvious as possible so I don't miss them. That's the reason why PBP makes an exception for them in end-of-line conditionals and loops.

Re:Obscure

masak on 2008-12-21T15:45:58

I can certainly understand that objection. Readability is important, and inventing new control flow "builtins" is a risky business.

However, it's also possible to reason the other way: I expect any conscientious reader to familiarize themselves with all the local variables in my sub before trying to understand what it does. &flunk_move is one of those local variables; and it happens to be a code block controlling the program flow. Making use of PBP's rule for putting control flow on the left, serves to make this fact even clearer.

I suspect this is a pattern that, by its nature, won't please everybody. Some people will think that the ability to define control flow closures will be worth the slight hit to readability, some won't. TIMTOWTDI.

Re:Obscure

chromatic on 2008-12-21T18:36:54

This technique is open to abuse, but consider a function with several potential exit points, each of which need to release acquired resources. This technique offers a cleaner approach than a local-goto or cut-and-past resource release blocks.

Re:Obscure

ChrisDolan on 2008-12-21T19:34:54

Agreed, to a degree. Carl is trading clarity of control flow for a consistent level of abstraction. As long as the technique is used sparingly and locally, I think it can be a net win.

But I'll wager that someone will write a Perl6::Critic policy about it in the future, maybe to say that pointy blocks with far-reaching control statements should be simple or not exported beyond the lexical scope.

Re:Obscure

Aristotle on 2008-12-22T19:03:53

That seems like an odd attitude coming from someone programming in Perl. I know Python programmers who think statement modifiers are a terrible idea in any and all cases on principle. Where did we lose the idea of giving people enough rope to hang themselves if they so desire? Just because this particular abstraction facility is new to you does not make it dangerous. All abstraction facilities have abuse potential – practice shows which ones are very dangerous and which ones can offer enough potential benefit to make up for their potential drawbacks. FWIW, this particular thing has been possible in Ruby for a very long time and does not seem to be making news as a maintainability problem; I am not wary of it.

Personally, I like to think that it’s a matter of bringing some taste to the work of a programmer, and that programmers need to develop taste, restraint and style just as much writers do. For such taste there is no substitute. Java is what happens if you approach language design with the contrary assumptions.

Re:Obscure

ChrisDolan on 2008-12-22T20:17:07

You're preaching to the choir. I'm just on the other end of the style continuum from you, I guess. I work with enough less-experienced programmers to prefer simple solutions, all else being (roughly) equal.

I'm not saying the practice should be *banned* from the language. I just don't want to see it in any code I must maintain. Action at a distance is the problem -- if the distance is small enough, then it's probably fine. On first read, I personally didn't think the solution was beneficial enough to justify the obfuscation. Carl has swayed my opinion from opposition to neutrality by pointing out he made the block a lexical.

Indeed, Perl 6 will have plenty of rope just like Perl 5. That's why Perl::Critic policies are all optional and should stay that way.