Solutions and Discussion for Perl Quiz of the Week #1 (Expert Edition)

Mark-Jason Dominus <[email protected]> Mon, 21 Oct 2002 15:03:19 -0400
Newsgroups gmane.comp.lang.perl.qotw.quiz-of-the-week
Organization Plover Systems
Message-ID <20021021190319.15156.qmail__1018.02710346437$1042944423@plover.com>
Sample solutions and discussion
Perl Expert Quiz of The Week #1 (20021016)

        Write a subroutine, 'subst', which gets a string argument, $s.  It
        should search $s and replace any occurrences of "$1" with the current
        value of $1, any occurrences of "$2" with the current value of $2, and
        so on.

        For example, if $1, $2, and $3 happen to be "dogs", "fish" and
        "carrots", then

                subst('$2, $1 and $3')

        should return

                "fish, dogs, and carrots"



I thought this problem would be harder than it was.  The obvious way
to analyze the argument string (let's call it the 'template') is to
use a regex, but if you use regexes, you'll destroy the contents of
$1, $2, etc., which you need for later.  Or so I thought; I had
forgotten that $1 etc. are dynamically block scoped, so that

        { 
          mess up $1 in here
        }
        $1 is restored here

This makes the solution rather easier than I had expected.

The solution I worked out beforehand looked something like this:

        sub subst {
          my $pat = shift;
          my @pos;
          while ($pat =~ /\$(\d+)/g) {
            push @pos, [$1, pos($pat)-2, 1+length $1] ;
          }
          for (reverse @pos) {
            my ($n, $pos, $len) = @$_;
            substr($pat, $pos, $len) = $$n;
          }
          $pat;
        }

The 'while' loop scans over the template looking for escape sequences;
whenever it finds one, it records its position in the string.  For the
example template '$2, $1, and $3', it builds this structure:

        [2, 0, 2],      # this means that '$2' appears at position 0
        [1, 4, 2],      # this means that '$1' appears at position 4
        [3, 12, 2],     # this means that '$3' appears at position 12

Each element contains the variable to be substituted, and the position
and length at which it should be inserted into the template.  The
position and length are just what 'substr' wants when it does a string
replacement, so it's easy to use 'substr' later.

The 'while' loop overwrites $1, but that's OK, because the original $1
is restored when the loop finishes.  

The 'for' loop then replaces the escape sequences in the template with
the appropriate values.  $$n is a symbolic reference, so if you were
using 'strict', you need 'no strict 'refs'' just before this line.

Doing the replacements in reverse order prevents the replacements from
messing up the string positions of the variables you will be replacing
later.  In general, replacing part of a string might move anything to
the right of the place where you did the replacement.  So we just do
the replacements from right to left.  This is probably the most
generally applicable trick I used; if you only learn one thing from
this example, this should be it.

1. Several people did analogous solutions which began by recording the
   $1, $2... variables into an array, and then replaced the escape
   seqences with the values of the array elements.  This seems to be
   simpler.  The trick is that you then have to figure out how big the
   array should be.  This works:

        sub subst {
          my $target = shift;
          my @n;
          for (1 .. $#+) {
            $n[$_] = $$_;
          }
          $target =~ s/\$(\d+)/$n[$1]/g;
          return $target;
        }

   It uses the Perl @+ variable to decide how many of $1, $2,
   etc. might be defined.

2. A few people tried a solution like the one in the previous section,
   but instead of using something like @+ to determine the number of
   $1, $2, ... variables, they just scanned through them until they
   found one that was undefined:

        sub subst {
                my $x=shift;
                my @p;
                for (my $i=1; eval 'defined($main::'.$i.')'; $i++) {
                        $p[$i]=eval '$main::'.$i;
                }
                $x=~s/\$(\d+)/$p[$1]/g;
                $x;
        }

   This gentleman said:

        my expert solutions assumes there are no gaps between
        $<number>'s, but i think it's quite reasonable. the only sane
        way to put values into this variables doesn't allow you to
        create gaps in numeration. 

   Unfortunately, he's mistaken.  Someone posted a complicated
   counterexample, but there's a really simple counterexample:

        "y" =~ /(x)|(y)/

   After this, $1 is undefined and $2 is "y".  

   So the 'scan while defined' approach doesn't work, even for quite
   ordinary cases.

3. Quite a few people tried to use 'eval'.  For example:

        sub subst($) { return eval( '"'.$_[0].'"' ); }

   This works very badly.  Among its defects:

        subst('I said, "Don\'t touch the $3!"');

   This should return

        I said, "Don't touch the carrots!"

   instead, it returns undef and delivers the message

        Bareword found where operator expected at (eval 1) line 1, near ""I said, "Don't"
        	(Missing operator before Don't?)

   on standard error.  Euggh.

   Another defect: subst('\n$=') should return the template unchanged,
   since there are no appearances of $1, $2, $3, etc.  The template is
   four characters long, consisting of <backslash> <n> <dollar>
   <equals>.  Instead, it returns the three-character string <newline>
   <6> <0>. 

   A third defect:  

        subst('".`cat /etc/passwd`."') 

   inserts the password file into the return value.  If you were to
   replace 'cat /etc/passwd' with 'rm -rf /', it would try to remove
   every file in the filesystem.  This is not what you normally expect
   from a function whose job is to fill out templates.

   A good rule of thumb is that unless what you're trying to do is
   most clearly described as "compile and run arbitrary Perl code",
   it's probably a mistake to use 'eval' to do it.

4. Shlomi Fish pointed out that where my sample solution had \d+, it
   should have been [1-9]\d*, since \d+ allows variables like $0 and
   $003, which were not mentioned in the original problem statement.

5. A number of people preferred to use something like 

        eval '$main::' . $n;

   to get the values of $1, $2, etc., rather than a symbolic reference
   like $$n.  Usually the motivation was that '$$n' does not work
   under 'strict refs'.

   This is misguided.  'strict refs' compliance is not an end in
   itself.  A 'strict refs' is a warning that you might be
   accidentally using a string as if it were a reference.  Here, we
   are not doing it accidentally.

   Use of strings as references is a bad move for two reasons.  First,
   it may be accidental, leading to bugs that are extremely difficult
   to find.   That is not an issue here.  

   Second, it's possible to make a mistake in the string and end up
   using a variable other than the one you meant to use.  For example,
   consider 

        ($var_name, $value) = get_input(...);
        $$var_name = $value;

   Here the intent is to ask the user for the name of a variable, and
   a value to assign to it, and then to perform the assignment.  If
   'get_input' forgets to chomp $var_name before returning it, then
   the assignment is actually made to a variable with a very strange
   name--- ${"x\n"} instead of $x for example.  Tracking down this
   sort of thing can be very difficult.  But using 'eval' as a
   replacement for the symbolic reference here makes the problem
   worse, not better:

        eval "\$$var_name = $value";

   Similarly, the symbolic reference code has a potential problem in
   that it may allow the user to overwrite Perl's special variables
   like $= or $\ .  But again, the 'eval' solution makes the problem
   worse; not only is the user still able to overwrite $\, they can
   also execute arbitrary code.

   It seems to me that over the past few years the Perl community has
   moved towards a rather bizarre
   "must-use-strict-everywhere-at-all-costs" position, even when the
   costs of 'strict' are to make the problems---the same problems it
   was put in to solve---even worse.  

   The metaphor I use for this in my classes is that you have met
   someone who says "My solution is better than yours because I can
   leave the smoke detector switched on the whole time and it never
   makes any noise," and then you look at their solution and find out
   that it starts by putting a giant fan in the room to blow all the
   smoke away from the detector.  This person remembers that the smoke
   detector is there to protect them, but they have forgotten what it
   is there to protect them from.  They have confused the outward
   signs of safety (a quiet detector) with actual safety.

   Symbolic references are a perfectly good way to solve this problem.
   There are good ways to solve it without using symbolic references,
   but 'eval' isn't one of them.

6. The most popular one of these ways was to use @- and @+, which tell
   you the start and end positions of $1, $2, etc. in the original
   target string.  For example:

        sub subst
        {
          my $retVal = shift;
          my @matches = map { substr($&, $-[$_], $+[$_]-$-[$_]) } 1..$#-;
          $retVal =~ s!\$(\d+)!$matches[$1-1]!g;
          $retVal;
        }

   Unfortunately, the original target string isn't easily available to
   subst().  This example assumes it's in '$&'.  But $& is only the
   *part* of the target string that matched, so, for example, although
   this works properly:

        "fish dogs carrots" =~ /(\w+) (\w+) (\w+)/;
        print subst('$2, $1 and $3');

   This doesn't:

        "---fish dogs carrots" =~ /(\w+) (\w+) (\w+)/;
        print subst('$2, $1 and $3');

   It prints:

        s ca, h do and rots

   (This is another one of those odd cases where people on the list
   posted a very complicated counterexample, when a very simple one
   was available.)  The problem goes away if you use

          my @matches = map { substr("$`$&$'", $-[$_], $+[$_]-$-[$_]) } 1..$#-;

   instead.  The big drawback of all these solutions is that any use
   of $&, $`, or $' anywhere in the program slows down all the other
   regexes everywhere.

7. Another alternative to symbolic references suggested by Yitzchak
   Scott-Thoennes is to visit the Perl symbol table directly, using
   '${$::{$n}}' instead of $$n.  Unfortunately, this doesn't work in
   this case, because for $1, etc. Perl only creates symbol table
   entries for the variables that are accessed symbolically.

        "fish dogs carrots" =~ /(\w+) (\w+) (\w+)/;
        $x = "$1 $3";
        for (1..3) {
          print "$_: $ {$::{$_}}\n";
        }

   This prints

        1: fish
        2:
        3: carrots

   Where did $2 go?  Well, you never used $2 in your program, so
   there's no symbol table entry for it.  If you comment out the "$x = ..." 
   line, the fish and carrots disappear too.

   But in any case, even when it works, you're still accessing the
   variable symbolically, and so it has all of the drawbacks of the
   simple symbolic reference version.  In this case, I don't think
   those are really drawbacks at all, but if you think this is in any
   way superior to the symbolic references version just because it
   doesn't provoke a 'strict refs' failure, you're deceiving yourself.

8. Some people went further, to consider whether subst() itself should
   provide an escaping mechanism, so that, for example

        subst('The value of \$3 is "$3".')

   would return

        The value of $3 is "carrots".

   instead of 

        The value of \carrots is "carrots".

   This is tricky to get right, because if you're going to support \$,
   you need to support \\ also, and then you start to have to deal
   with arguments like

        The value of \\\\\\\\\\\$3 is "\\\\\\\\\\$3".

   Some people presented regexes that do this:

          $s =~ s/(?<!
                      (?<! \\ )
                      (\\ \\ )*
                      \\ 
                  )
                  \$(\d+)
                 /$saves[$1]/gx;

   Unfortunately, they didn't try these, because:

        Variable length lookbehind not implemented

   (Thanks to Jeff Pinyan for pointing this out.)

   Even if Perl did support variable length lookbehind, this still
   wouldn't be quite right, because someone who calls 'subst' on a
   string with four backslashes will be expecting to get only two back
   again.  (Just as in Perl, 'print "\\\\"' prints only two backslashes.)

9. Sweth Chandramouli actually implemented a full lexer to handle
   everything backslashy.  Sweth's solution is 50 lines of rather
   C-like Perl code.  I wasn't sure whether to include it, so I
   included it:

        sub subst {
           my $input_string = shift;
           my $current_state = "REGULAR";
           my ($output_string, $escaped, $numvar);
           while (
              my $next_char = substr ($input_string, 0, 1, '')
                 or
              0 == length ($input_string) 
           ) {
              if ($current_state eq 'REGULAR') {
                 if ($next_char eq '$') {
                    $current_state = 'DOLLAR';
                 } elsif ($next_char eq '\\') {
                    if ($escaped) {
                       $output_string .= '\\';
                       $escaped = 0;
                    } else {
                       $escaped = 1;
                    };
                 } else {
                    if ($escaped) {
                       $output_string .= '\\';
                       $escaped = 0;
                    };
                    $output_string .= $next_char;
                 };
              } elsif ($current_state eq 'DOLLAR') {
                 if ($next_char =~ /\d/) {
                    $numvar .= $next_char;
                 } else {
                    if (length ($numvar)) {
                       if ($escaped) {
                          $output_string .= '$';
                          $output_string .= $numvar;
                          $output_string .= $next_char;
                          $escaped = 0;
                       } else {
                          eval "\$output_string .= \$$numvar";
                          $output_string .= $next_char;
                       };
                       $numvar = '';
                    } else {
                       if ($escaped) {
                          $output_string .= '\\';
                          $escaped = 0;
                       };
                       $output_string .= '$';
                       $output_string .= $next_char;
                    };
                    $current_state = 'REGULAR';
                 };
              };         
              if (
                 0 == length ($input_string)
                    and
                 0 == length ($next_char)
              ) {
                 last;
              };
           };
           return ($output_string);
        };

   Here's what I said to Sweth:  

        My experience is that when you build a state-driven
        character-by-character lexical analyzer in Perl, as you have
        done here, you are almost always making a mistake.  The reason
        is that Perl already has a very powerful state-driven
        character-by-character analysis engine built into it: this is
        precisely what the regex engine does when it looks for a
        match.  Almost all such analysis can be done quicker and more
        simply with regexes.

        In fact, it used to be that when people asked me if Perl was
        good for everything, I would say no, it wasn't, that it was
        bad for writing lexers because it was too slow and clumsy.
        But then I found out I had been doing it all wrong and I don't
        give that answer any more.

   Then I had to see what the regex solution would look like:

        sub subst {
          my $template = shift;
          pos($template) = 0;
          my @result;
          until ($template =~ /\G\z/gc) {
            push(@result,  $1),  next if $template =~ /\G\\(.)/sgc;
            push(@result, [$1]), next if $template =~ /\G\$([1-9]\d*)/gc;
            push(@result,  $1),  next if $template =~ /\G(.)/sgc;
            die "Lexing error that I don't understand";
          }
          for (@result) {
            $_ = $ {$_->[0]} if ref $_;
          }
          return join "", @result;
        }

   I was surprised and delighted to see how short this was.

   We have to be a little tricky here because while the regex matching
   is going on, we don't have access to the original $1, $2, etc.  So
   instead we build up an array @result, which contains strings, which
   are inserted into the output literally, and little arrays, [1],
   [2], etc., which indicate that the value of the corresponding $1,
   $2, etc. variable should be inserted at that point.

10. Analogous to this, but smaller, probably more efficient, and
    almost certainly easier to understand, was a solution suggested by
    Jeff Pinyan:

        sub subst {
          my $template = shift;
          my @vals;

          for (1 .. $#+) {
            $vals[$_] = $$_;
          }

          $template =~ s{ \\(.) | \$([1-9]\d*) }{
            if (defined $1) { $1 }
            else { $vals[$2] }
          }gex;

          $template;
        }

    Instead of using the rather obscure '/\G.../gc' feature to find
    parts of the target string we want, we just use 's///', which
    everyone understands.  The s/// is looking for two kinds of things
    to substitute: A backslash followed by a single character, in
    which case the backslash is removed, or a $ followed by a numeral,
    in which case the approrpaite value is substituted.  Everything
    else stays the same.

Thanks again to everyone who subscribed, and especially to those who
participated in the discussion.  I'll send another quiz on Wednesday.