《高阶Perl》——2.2 计算器

2.2 计算器

暂时不讲配置文件的例子了。显然,分配表将在许多类似的情形中有其意义。例如,一个必须执行来自用户的命令的对话程序能使用一个分配表分配用户的命令。接下来介绍一个不同的例子,一个非常简单的计算器。

输入此计算器的是一串以逆波兰表示法(Reverse Polish Notation,RPN)表示的算术表达式。传统的算术标记法是有歧义的。如果你写下2+3·4,那到底是先做加法还是乘法,并不清楚。因此必须特别约定乘法总是比加法先做,或者必须插入括号消除表达式的歧义,例如,(2+3)·4。

逆波兰表示法以另一种方式解决问题。不是把操作符放在它们操作的参数的中间,而是放在后面。例如,2+3写成2 3 +。(2+3)·4写成2 3 + 4 。2和3后面是+,所以2和3相加;表示之前的两个表达式相乘,即2 3 +和4。要以RPN表示2+(3·4),可以写成2 3 4 +。+应用于之前的两个参数,第一个是2,第二个是3 4 。因为操作符总是在它的参数之后,这样的表达式就称为后缀式(postfix form);对照于此,通常的操作符在中间的表达式,就称为中缀式(infix form)。

计算RPN表达式的值很容易。为此,维护一个栈,然后从左往右读取表达式。当看到一个数字时,要把它压入栈。当看到一个操作符时,弹出栈顶部的两个元素,对它们进行操作,然后把结果压回栈。例如,要计算2 3 + 4 ,需要先压入2然后压入3,然后当看到+时就弹出它们并把总和5压回栈。然后把4压到5的上面,然后说明要弹出4和5并把最后的结果20压回。要计算2 3 4 +需要压入2,然后是3,然后是4。说明要弹出3和4并把乘积12压回,+说明要弹出12和2并压回总和14,这是最终的答案。

这是一个小的计算器程序,它计算在它的命令行参数中给出的RPN表达式:

### Code Library: rpn-ifelse
my $result = evaluate($ARGV[0]);
print "Result: $result\n";

sub evaluate {
  my @stack;
  my ($expr) = @_;
  my @tokens = split /\s+/, $expr;
  for my $token (@tokens) {
    if ($token =~ /^\d+$/) { # It's a number
      push @stack, $token;
    } elsif ($token eq '+') {
      push @stack, pop(@stack) + pop(@stack);
    } elsif ($token eq '-') {
      my $s = pop(@stack);
      push @stack, pop(@stack) - $s
    } elsif ($token eq '*') {
      push @stack, pop(@stack) * pop(@stack);
    } elsif ($token eq '/') {
      my $s = pop(@stack);
      push @stack, pop(@stack) / $s
    } else {
      die "Unrecognized token '$token'; aborting";
    }
  }
  return pop(@stack);
}

这个函数用空白符把参数分隔成记号(token),这是最小的、有意义的输入部分。然后函数从左往右每次循环一个记号。如果一个记号匹配/^d+$/,那么它是一个数,因此函数把它压入栈。否则,它是一个操作符,因此函数从栈里弹出两个值,操作它们,并把结果压回栈。减法部分的代码里有辅助变量$s是因为5 3 -应该产生2,而不是-2。如果用了:

push @stack, pop(@stack) - pop(@stack);

那么对于5 3 -,第一个pop弹出3,第二个pop弹出5,结果会是-2。同理,类似的代码也出现在除法部分。对于乘法和加法,操作数的次序无关紧要。

当函数读完记号,它就弹出栈顶部的值,这就是最后的结果。这段代码忽略了栈结束时也许有几个值的可能,这意味着,参数包含不止一个表达式。10 2 3 4 +在栈里依次留下了20和7。它也忽略了栈也许变空的可能。例如,2 和2 3 + 就是无效的表达式,因为其中只有一个参数,而不是两个。在计算这些的时候,函数发现当栈空时它自己还在做操作。在那种情况下,它应当发出错误信号,但是我忽略了错误处理以保持例子的短小精悍。

可以通过把巨大的if-else分支替换成分配表,使例子更简洁更灵活:

### Code Library: rpn-table
my @stack;
my $actions = {
  '+' => sub { push @stack, pop(@stack) + pop(@stack) },
  '*' => sub { push @stack, pop(@stack) * pop(@stack) },
  '-' => sub { my $s = pop(@stack); push @stack, pop(@stack) - $s },
  '/' => sub { my $s = pop(@stack); push @stack, pop(@stack) / $s },
  'NUMBER' => sub { push @stack, $_[0] },
  '_DEFAULT_' => sub { die "Unrecognized token '$_[0]'; aborting" }
};

my $result = evaluate($ARGV[0], $actions);
print "Result: $result\n";

sub evaluate {
  my ($expr, $actions) = @_;
  my @tokens = split /\s+/, $expr;
  for my $token (@tokens) {
    my $type;
    if ($token =~ /^\d+$/) { # It's a number
      $type = 'NUMBER';
    }

    my $action = $actions->{$type}
              || $actions->{$token}
              || $actions->{_DEFAULT_};
    $action->($token, $type, $actions);
  }
  return pop(@stack);
}

主要的驱动,evaluate(),现在更小巧更通用了。它基于记号的“类型”选择一个行为,如果后者有一个行为;否则,行为就基于记号本身的值,如果不存在这样的行为,就使用一个默认的行为。函数evaluate()对记号做模式匹配以尝试确定记号的类型,如果记号看起来像一个数,那么选择的类型就是NUMBER。可以在%actions分配表里增加一条以增加一个新的操作符:

...
'sqrt' => sub { push @stack, sqrt(pop(@stack)) },
...

同样,由于分配表的结构,提供不同的分配表给求值程序就可以得到不同的行为。如果提供如下分配表,求值程序就会把表达式编译成抽象语法树(Abstract Syntax Tree,AST),而不是把表达式换算成一个数:

my $actions = {
  'NUMBER'    => sub { push @stack, $_[0] },
  '_DEFAULT_' => sub { my $s = pop(@stack);
                       push @stack,
                          [ $_[0], pop(@stack), $s ]
                     },
};

编译2 3 + 4 的结果是抽象语法树[ '', [ '+', 2, 3 ], 4 ],也可以用图2-1表示。

image

这是对表达式最有用的内部形式,因为所有的结构直接明了。表达式或者是一个数,或者它有一个操作符和两个操作数,这两个操作数也是表达式。抽象语法树或是一个数,或是一系列的操作符与另两个AST。一旦有了AST,很容易写一个函数处理它。例如,这里有个函数把AST转换成字符串:

### Code Library: AST-to-string
sub AST_to_string {
  my ($tree) = @_;
  if (ref $tree) {
    my ($op, $a1, $a2) = @$tree;
    my ($s1, $s2) = (AST_to_string($a1),
                     AST_to_string($a2));
    "($s1 $op $s2)";
  } else {
    $tree;
  }
}

对于图2-1的树,函数AST_to_string()就会产生字符串"((2 + 3) * 4)"。函数首先检查树是否是平凡的,如果它不是一个引用,那它必是一个数,字符串的版本就是那个数。否则,字符串有三部分:一个操作符符号,存放在$op中,以及两个参数,即AST。函数递归调用自身把两棵参数树转换成字符串$s1和$s2,然后产生一个新的字符串,含有$s1、$s2,以及它们中间的合适的操作符符号,并在左右两侧加上了括号以避免歧义。已经写了一个系统把后缀表达式转换成中缀表达式,因为可以把原始的后缀表达式赋值给evaluate()以产生一个AST,然后把AST给AST_to_string()以产生一个中缀表达式。

函数AST_to_string()是递归的是因为AST的定义是递归的,AST定义是递归的是因为表达式的结构是递归的。AST_to_string()的结构直接反映了表达式的结构。

2.2.1 再访 HTML 处理

第1章介绍了walk_html(),一个递归的HTML处理器。HTML处理器得到两个函数参数:$textfunc,一个用来处理未置标签的文本片段的函数,以及$elementfunc,一个用来处理HTML元素的函数。但是“HTML元素”是笼统的,因为有许多种类的元素,希望函数对每类不同的元素能做不同的事情。

我们已经看到了几种完成此类处理的方法。对用户而言,最直接的方法就是简单地在$elementfunc里放个巨大的if-else分支。但是已经看到,那样做会有一些缺点。用户可能更喜欢提供一个分配表给$elementfunc。这样的一个分配表的结构显而易见:表的键将是标签名称,值将是对每类元素执行的行为。用户不是提供单个能知道如何处理所有可能的元素的$elementfunc,相反,用户将提供一个分配表,为每类元素提供一个行为,即一个普通的$elementfunc分配适当的行为。

$elementfunc可以用几种方法获得分配表。分配表可以硬编码在这个元素函数里:

sub elementfunc {
  my $table = { h1        => sub { shift; my $text = join '', @_;
                                   print $text; return $text ;
                                 }
                _DEFAULT_ => sub { shift; my $text = join '', @_;
                                              return $text ;
                                 }
              };
  my ($element) = @_;
  my $tag = $element->{_tag};
  my $action = $table->{$tag} || $table->{_DEFAULT_};
  return $action->(@_);
}

或者,可以直接在walk_html()里直接支持分配表,那样用户就不用传递单个$elementfunc,而是直接传递分配表给walk_html()。在那种情况下。walk_html()如下:

### Code Library: walk-html-disp
sub walk_html {
  my ($html, $textfunc, $elementfunc_table) = @_;
  return $textfunc->($html) unless ref $html; # It's a plain string

  my ($item, @results);
  for $item (@{$html->{_content}}) {
    push @results, walk_html($item, $textfunc, $elementfunc_table);
  }
  my $tag = $html->{_tag};
  my $elementfunc = $elementfunc_table->{$tag}
                 || $elementfunc_table->{_DEFAULT_}
                 || die "No function defined for tag '$tag'";
  return $elementfunc->($html, @results);
}

还有一种做法是把walk_html()改成传递一个用户形参给$textfunc和$elementfunc。然后用户可以通过用户形参机制把分配表传递给$elementfunc:

### Code Library: walk-html-uparam
sub walk_html {
  my ($html, $textfunc, $elementfunc, $userparam) = @_;
  return $textfunc->($html, $userparam) unless ref $html;
  my ($item, @results);
  for $item (@{$html->{_content}}) {
    push @results, walk_html($item, $textfunc, $elementfunc, $userparam);
  }
  return $elementfunc->($html, $userparam, @results);
}

现在如何设计他们的$elementfuncs以适当地处理分配表,由用户决定。

有一点是重要的和巧妙的:传递了和$elementfunc一样的用户形参给$textfunc。如果用户形参是一个标签分配表,它可能对$textfunc没有用。那么为什么要传递它呢?因为它可能不是一个标签表,它可能是别的东西。例如,用户可能像这样调用walk_html():

walk_html($html_text,

          # $textfunc
          sub { my ($text, $aref) = @_;
                push @$aref, $text },

          # $elementfunc does nothing
          sub { },

          # user parameter
          \@text_array
         );

现在walk_html()将遍历HTML树并把所有没有标签的普通文本压入数组@text_array。用户形参是指向@textarray的引用,把它传递给$textfunc,后者把文本压入指向的数组。$elementfunc根本不使用用户形参。因为walk_html()的作者无法预知用户将需要哪类用户形参,最好把它都传递给$textfunc与$elementfunc,不需要用户形参的函数可以随意忽略它。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值