暂时不讲配置文件的例子了。显然,分配表将在许多类似的情形中有其意义。例如,一个必须执行来自用户的命令的对话程序能使用一个分配表分配用户的命令。接下来介绍一个不同的例子,一个非常简单的计算器。
输入此计算器的是一串以逆波兰表示法(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。可以在