《高阶Perl》——第2章 分配表 2.1 配置文件处理

第2章

分 配 表

第1章介绍了如何用别的函数参数化函数的行为使函数更加灵活。例如,并没有把每次移动盘子就输出一条消息硬编码到hanoi()函数里,而是让其调用一个从外部传入的辅助函数。通过提供一个合适的辅助函数,可以使hanoi()输出一系列说明,或检查它自己的行动,或生成一个图形显示,而不必重新编写基本的算法。类似地,可以从total_size()函数的计算文件大小的行为中提取出目录遍历行为,得到一个更有价值和普遍适用的dir_walk()函数,它可以做许多不同的事情。

为了从hanoi()与dir_walk()提取出行为,使用了代码引用。把别的函数作为参数传递给hanoi()与dir_walk()函数,有效地把辅助函数当成数据块。代码引用使这些成为可能。

现在先不讲递归,而叙述代码引用的另一种用法。

2.1 配置文件处理

假设我们有一个应用要读取一个如下格式的配置文件:

VERBOSITY       8
CHDIR           /usr/local/app
LOGFILE         log
...             ...

要读取这个配置文件并根据每个指示采取适当的行动。例如,对于VERBOSITY指示,只是设置一个全局变量。而对于LOGFILE指示,则要立即重定向程序的诊断消息到指定的文件。对于CHDIR,也许可以让程序chdir指定的目录以使随后的文件操作与新的目录相关联。这意味着,在之前的例子里LOGFILE是/usr/local/app/log,而不是用户在程序运行时恰好所在的目录下的log文件。

许多程序员会遇到这个问题并会立即想象到一个含有巨大if-else分支的函数,如下:

sub read_config {
  my ($filename) = @_;
  open my($CF), $filename or return; # Failure
  while (<$CF>) {
    chomp;
    my ($directive, $rest) = split /\s+/, $_, 2;
    if ($directive eq 'CHDIR') {
      chdir($rest) or die "Couldn't chdir to '$rest': $!; aborting";
    } elsif ($directive eq 'LOGFILE') {
      open STDERR, ">>", $rest
        or die "Couldn't open log file '$rest': $!; aborting";
    } elsif ($directive eq 'VERBOSITY') {
      $VERBOSITY = $rest;
    } elsif ($directive eq ...) {
      ...
    } ...
    } else {
      die "Unrecognized directive $directive on line $. of $filename; aborting";
    }
  }
  return 1; # Success
}

这个函数分为两部分。第一部分打开文件并每次从中读取一行。它把每行分成$directive部分(第一个单词)和$rest部分(剩余的部分)。$rest部分包含了指示的参数,如提供给LOGFILE指示的要打开的日志文件名。函数的第二部分是一棵大的if-else树,它检查$directive变量,查看它是哪个指示,如果指示不可识别,则中断程序。

这类函数可以变得非常庞大,因为在if-else树中有许多选项。每次有人想增加一个指示,他就要改变函数增加一个elsif分句。if-else树的分枝的内容相互之间没有很多事情要做,除了它们都是可配置的琐碎事实。这样的函数违背了编程的一条重要法则:相关的东西应该放在一起;不相关的东西应该分开。

依照此法则为这个函数提出了一个不同的结构:读取和解析文件的部分应该与配置的指示被识别后的执行动作分开。此外,实现各种不相关的指示的代码不应该一起挤进单个函数。

2.1.1 表驱动配置

可以把打开、读取和解析配置文件的代码与实现不同指示的不相关的代码分开。像这样把程序分成两半将可以更加灵活地修改每部分,也把代码与指示分开了。

有read_config()的一个替代版本:

### Code Library: rdconfig-tabular
sub read_config {
  my ($filename, $actions) = @_;
  open my($CF), $filename or return; # Failure
  while (<$CF>) {
    chomp;
    my ($directive, $rest) = split /\s+/, $_, 2;
    if (exists $actions->{$directive}) {
      $actions->{$directive}->($rest);
    } else {
      die "Unrecognized directive $directive on line $. of $filename; aborting";
    }
  }
  return 1; # Success
}

和之前完全一样地打开、读取和解析配置文件。但不再依赖巨大的if-else分支了。而这版read_config接受一个额外的参数,$actions,它是一个行动表,read_config()每读取一个配置的指示,它将执行这些行动之一。这个表就称为分配表(dispatch table),因为它包含了read_config()读文件时将要把控制分配到的函数。变量$rest的意义和之前相同,但现在作为一个参数传递给合适的行为函数。

一个典型的分配表如下:

$dispatch_table =
{ CHDIR      => \&change_dir,
  LOGFILE    => \&open_log_file,
  VERBOSITY  => \&set_verbosity,
  ...        => ...,
};

分配表是一个散列,它的键(通常称为标签(tag))是指示的名称,它的值是行为(action),指向当识别出合适的指示名时调用的子例程。行为函数期望接受变量$rest作为一个参数,典型的行为如下:

sub change_dir {
  my ($dir) = @_;
  chdir($dir)
    or die "Couldn't chdir to '$dir': $!; aborting";
}

sub open_log_file {
  open STDERR, ">>", $_[0]
    or die "Couldn't open log file '$_[0]': $!; aborting";
}

sub set_verbosity {
  $VERBOSITY = shift
}

如果行为很小,就可以直接把它们放到分配表里:

$dispatch_table =
  { CHDIR      => sub { my ($dir) = @_;
                       chdir($dir) or
                         die "Couldn't chdir to '$dir': $!; aborting";
                      },
    LOGFILE    => sub { open STDERR, ">>", $_[0] or
                          die "Couldn't open log file '$_[0]': $!; aborting";
                      },
    VERBOSITY  => sub { $VERBOSITY = shift },
    ...        => ...,
};

通过转变为一个分配表,消除了巨大的if-else树,但是到头来还是得到了一个只小了一点的表。这看起来不太成功。但是表带来了几个好处。

2.1.2 分配表的优势

分配表是数据,而不是代码,所以它可以在运行时改变。你可以在你想的任何时候插入新的指示到表里。假设表含有:

'DEFINE' => \&define_config_directive,

其中,define_config_directive()是:

### Code Library: def-conf-dir
sub define_config_directive {
  my $rest = shift;
  $rest =~ s/^\s+//;
  my ($new_directive, $def_txt) = split /\s+/, $rest, 2;

  if (exists $CONFIG_DIRECTIVE_TABLE{$new_directive}) {
    warn "$new_directive already defined; skipping.\n";
    return;
  }

  my $def = eval "sub { $def_txt }";
  if (not defined $def) {
    warn "Could not compile definition for '$new_directive': $@; skipping.\n";
    return;
  }

  $CONFIG_DIRECTIVE_TABLE{$new_directive} = $def;
}

配置器现在接受这样的指示:

DEFINE HOME       chdir('/usr/local/app');

define_config_directive()把HOME放入$new_directive并把chdir('/usr/local/app');放入$def_txt。它用eval把定义文本编译成一个子例程,然后把新的子例程装入一个主配置表,%CONFIG_DIRECTIVE_TABLE,以HOME为键。如果事实上%CONFIG_DIRECTIVE_TABLE是一开始就传递给read_config()的分配表,那么read_config()将会看到新的定义,如果在输入文件的下一行看到指示HOME,就将把一个行为关联到HOME。现在一个配置文件如下:

DEFINE HOME       chdir('/usr/local/app');
CHDIR /some/directory
...
HOME

在...里的指示是在目录/some/directory里被执行。当处理器到达HOME时,它就返回到它的家目录。也可以定义一个相同的但更健壮的版本:

DEFINE PUSHDIR  use Cwd; push @dirs, cwd(); chdir($_[0])
DEFINE POPDIR   chdir(pop @dirs)

PUSHDIR dir用标准Cwd模块提供的cwd()函数指出当前目录的名称。它把当前目录的名称保存在变量@dirs里,然后改变到目录dir。POPDIR撤销最后一个PUSHDIR的影响:

PUSHDIR /tmp
A
PUSHDIR /usr/local/app
B
POPDIR
C
POPDIR

程序改变到/tmp,执行指示A。然后改变到/usr/local/app并执行指示B。随后的POPDIR使程序回到/tmp,在那里执行指示C,最后第二个POPDIR使程序回到它开始的地方。

为了使DEFINE能改变配置表,将不得不把它存入一个全局变量。如果明确地把表传递给define_config_directive也许更好。为此需要对read_config做一点小小的改变:

### Code Library: rdconfig-tablearg
sub read_config {
  my ($filename, $actions) = @_;
  open my($CF), $filename or return; # Failure
  while (<$CF>) {
    chomp;
    my ($directive, $rest) = split /\s+/, $_, 2;
    if (exists $actions->{$directive}) {
      $actions->{$directive}->($rest, $actions);
    } else {
      die "Unrecognized directive $directive on line $. of $filename; aborting";
    }
  }
  return 1; # Success
}

现在define_config_directive如下:

### Code Library: def-cdir-tablearg
sub define_config_directive {
  my ($rest, $dispatch_table) = @_;
  $rest =~ s/^\s+//;
  my ($new_directive, $def_txt) = split /\s+/, $rest, 2;

  if (exists $dispatch_table->{$new_directive}) {
    warn "$new_directive already defined; skipping.\n";
    return;
  }

  my $def = eval "sub { $def_txt }";
  if (not defined $def) {
    warn "Could not compile definition for '$new_directive': $@; skipping.\n";
    return;
  }

  $dispatch_table->{$new_directive} = $def;
}

有了这个改变,就可以增加一个确实有用的配置指示了:

DEFINE INCLUDE    read_config(@_);

它安装一个新的条目到分配表里,如下:

INCLUDE => sub { read_config(@_) }

现在,当在配置文件里写:

INCLUDE extra.conf

主函数read_config()将执行行为,传递给它两个参数。第一个参数是从配置文件里得到的$rest,在这个例子里是文件名extra.conf。第二个参数还是分配表。将把这两个参数直接传递给read_config的递归调用。read_config将读取extra.conf,当它结束时就会把控制交给read_config的主调用,后者将继续处理主要的配置文件,从刚才离开的地方继续。

为了递归调用能正确工作,read_config()必须是可重入的。破坏可重入性最简单的方法是使用全局变量,如使用一个全局文件句柄代替词法文件句柄。如果使用了一个全局文件句柄,递归调用read_config()将会用同样被主调用使用的句柄打开extra.conf,这将会关闭主配置文件。当递归调用返回时,read_config()将无法读取主文件的剩余部分,因为它的文件句柄已经关闭了。

INCLUDE这个定义非常简单也非常实用。但它也是巧妙的,也许写read_config的时候都没有意识到。“read_config不需要是可重入的”说起来简单。然而,如果已经写了不可重入的read_config,那么有用的INCLUDE定义将不会起作用。在这里可以学到一个重要的经验:默认使函数是可重入的,因为有时递归调用带来的好处将是一个惊喜。

可重入的函数展现了比不可重入的函数更简单和更可预见的行为。它们更加灵活因为它们可以递归地调用。INCLUDE例子表明无法总预见到所有的想递归地执行一个函数的理由。更好也更安全的是尽可能使所有函数是可重入的。

分配表与在read_config()里硬编码相比较,另一个优势是可以使用同一个read_config函数处理两个不相关并且有完全不同指示的文件,只要每次传递一个不同的分配表给read_config()。可以通过传递一个简装的分配表给read_config()而使程序处于“初学者模式”。或者可以重复利用read_config()处理另一个带有相同基本语法的文件,只要传递给它一个带有一套不同的指示的表即可。在2.1.4节有这样的一个例子。

2.1.3 分配表策略

在PUSHDIR与POPDIR实现中,行为函数使用了一个全局变量,@dirs, 维护压入的目录的栈。这效果不好。可以通过让read_config()支持一个用户形参(user parameter)克服它,使系统更灵活。这是一个参数,由read_config()的主调者提供,一字不变地传递给行为:

### Code Library: rdconfig-uparam
sub read_config {
  my ($filename, $actions, $user_param) = @_;
  open my($CF), $filename or return; # Failure
  while (<$CF>) {
    my ($directive, $rest) = split /\s+/, $_, 2;
    if (exists $actions->{$directive}) {
     $actions->{$directive}->($rest, $user_param, $actions);
    } else {
      die "Unrecognized directive $directive on line $. of $filename; aborting";
    }
  }
  return 1; # Success
}

这消除了全局变量,因为现在可以像这样定义PUSHDIR和POPDIR了:

DEFINE PUSHDIR  use Cwd; push @{$_[1]}, cwd(); chdir($_[0])
DEFINE POPDIR   chdir(pop @{$_[1]})

形参$_[1]指向被传递给read_config()的用户形参参数。如果read_config()这样调用:

read_config($filename, $dispatch_table, \@dirs);

那么PUSHDIR和POPDIR将用数组@dirs作为它们的栈,如果它这样调用:

read_config($filename, $dispatch_table, []);

那么它们将使用一个崭新的、匿名的数组作为栈。

向一个行为回调传递一个要执行的行为的标签名称常常是有用的。为此,可以改变read_config():

### Code Library: rdconfig-tagarg
sub read_config {
  my ($filename, $actions, $user_param) = @_;
  open my($CF), $filename or return; # Failure
  while (<$CF>) {
    my ($directive, $rest) = split /\s+/, $_, 2;
    if (exists $actions->{$directive}) {
      $actions->{$directive}->($directive, $rest, $actions, $user_param);
    } else {
      die "Unrecognized directive $directive on line $. of $filename; aborting";
    }
  }
  return 1; # Success
}

为什么这是有用的?参考为VERBOSITY指示定义的行为:

VERBOSITY => sub { $VERBOSITY = shift },

容易想象会有一些配置指示遵循这个通用模式:

VERBOSITY => sub { $VERBOSITY = shift },
TABLESIZE => sub { $TABLESIZE = shift },
PERLPATH  => sub { $PERLPATH = shift },
... etc ...

把这三个类似的行为合并成单个做这三件工作的函数。为此,函数需要知道指示的名称以便设置合适的全局变量:

VERBOSITY => \&set_var,
TABLESIZE => \&set_var,
PERLPATH  => \&set_var,
... etc ...

sub set_var {
  my ($var, $val) = @_;
  $$var = $val;
}

或者,如果你不喜欢一堆松散的全局变量,你可以把配置信息保存到一个散列里,然后传递这个散列的引用作为用户形参:

sub set_var {
  my ($var, $val, undef, $config_hash) = @_;
  $config_hash->{$var} = $val;
}

在这个例子里,节省的不多,因为行为如此简单。然而可能有几个配置指示需要共享一个更复杂的函数。这里有一个稍微复杂些的例子:

sub open_input_file {
  my ($handle, $filename) = @_;
  unless (open $handle, $filename) {
    warn "Couldn't open $handle file '$filename': $!; ignoring.\n";
  }
}

这个open_input_file()函数可以被许多配置指示分享。例如,假设一个程序有三个输入文件:一个历史文件、一个临时文件和一个模式文件。希望这三个文件的位置都可以在配置文件里配置,这需要在分配表里有三个条目。但是三个条目都可以共享相同的open_input_file()函数:

...
HISTORY  => \&open_input_file,
TEMPLATE => \&open_input_file,
PATTERN  => \&open_input_file,
...

现在假设配置文件认为:

HISTORY         /usr/local/app/history
TEMPLATE        /usr/local/app/templates/main.tmpl
PATTERN         /home/bill/app/patterns/default.pat

read_config()将看到第一行并分配给open_input_file()函数,传递给它的参数列表是('HISTORY','/usr/local/app/history')。open_input_file()将参数HISTORY

作为文件句柄名,并把HISTORY文件句柄打开到文件/usr/local/app/history。第二行,

read_config()将再次分配给open_input_file(),这次传递给它('TEMPLATE',
'/usr/local/app/templates/main.tmpl')。这次,open_input_file()将打开TEMPLATE

句柄而不是HISTORY句柄。

2.1.4 默认行为

例子中的read_config()函数一遇到无法识别的指示就会崩溃。这种行为是硬编码在其中的。如果分配表自身携带了对一个无法识别的指示要做什么的信息,那会更好。增加这个功能很简单:

### Code Library: rdconfig-default
sub read_config {
  my ($filename, $actions, $userparam) = @_;
  open my($CF), $filename or return; # Failure
  while (<$CF>) {
    chomp;
    my ($directive, $rest) = split /\s+/, $_, 2;
    my $action = $actions->{$directive} || $actions->{_DEFAULT_};
    if ($action) {
      $action->($directive, $rest, $actions, $userparam);
    } else {
      die "Unrecognized directive $directive on line $. of $filename; aborting";
    }
  }
  return 1; # Success
}

这里的函数在行为表里寻找指定的指示,如果没有,它就寻找_DEFAULT_行为,仅当分配表里没有指定的默认行为时崩溃。这里有一个典型的_DEFAULT_行为:

sub no_such_directive {
  my ($directive) = @_;
  warn "Unrecognized directive $directive at line $.; ignoring.\n";
}

由于把指示的名称作为第一个参数传递给行为函数,因此默认的行为知道调用无法识别的指示代表什么。由于no_such_directive()函数也得到了传递的整个分配表,因此它可以抽取到真实的指示名称并通过模式匹配指出可能的含义。这里no_such_directive()用一个假想的score_match()函数判断哪个表条目良好地匹配无法识别的指示:

sub no_such_directive {
  my ($bad, $rest, $table) = @_;
  my ($best_match, $best_score);
  for my $good (keys %$table) {
    my $score = score_match($bad, $good);
    if ($score > $best_score) {
      $best_score = $score;
      $best_match = $good;
    }
  }
  warn "Unrecognized directive $bad at line $.;\n";
  warn "\t(perhaps you meant $best_match?)\n";
}

现在拥有的系统只含有少量代码,但它是极其灵活的。假设程序还要读取一系列用户ID与电子邮件地址,格式如下:

fred            fred@example.com
bill            bvoehno@plover.com
warez           warez-admin@plover.com
...             ...

可以复用read_config()并让它读取和解析这个文件,通过提供合适的分配表:

$address_actions =
  { _DEFAULT_ => sub { my ($id, $addr, $act, $aref) = @_;
                       push @$aref, [$id, $addr];
                     },
  };

read_config($ADDRESS_FILE, $address_actions, \@address_array);

这里已经给了read_config()一个非常小的分配表,它只有一个_DEFAULT_条目。read_config()对地址文件里的每一行都将调用这个默认的条目一次,传递给它“指示名称”(实际上即用户ID)与地址(即$rest的值)。默认的行为将获得这些信息并增加到@address_array,程序可以在以后使用它。

时间: 2024-08-04 11:12:49

《高阶Perl》——第2章 分配表 2.1 配置文件处理的相关文章

《高阶Perl》——导读

前 言 在编程圈子里有一句著名的俗语,一个优秀的Fortran程序员可以用任何语言写Fortran程序.然而,让人悲哀的是,不管他们是否愿意,Fortran程序员用任何语言写Fortran程序.类似地,作为Perl程序员,我们也在用Perl写C程序,不管我们是否愿意.这让人羞愧,因为Perl是一门比C更富有表现力的语言.我们本可以做得更好,以C程序员梦想不到的方式使用Perl,但是我们没有那样做. 怎么会这样呢?Perl的设计初衷是一方面作为C的替代品,另一方面作为UNIX脚本语言(如Bourn

《高阶Perl》——1.2 阶乘

1.2 阶乘 假设有一个有n个不同条目的列表.为了具体点,假设这些条目是字母表的字母.这样一个列表有多少种不同的排列次序呢?显然,因为答案依赖于n,所以它是n的函数.这个函数称为阶乘函数(factorial function).n的阶乘就是n个不同条目的不同的排列次序的数量.通常以一个后缀!标记,这样n的阶乘就是n!.一般不同的次序称为排列(permutation). 下面计算一些阶乘.显然,只有一个条目的列表只有一种排列,所以1!=1.两个条目的列表有两种排列:A-B和B-A.少许笔算可以发现

《高阶Perl》——3.12 速度的好处

3.12 速度的好处 此刻有个说法比较诱人,那就是记忆术只比手动缓存技术改善了一点,因为它做了同样的事情,仅有的额外的好处是你可以快速启动或者关闭它.但这不完全正确.当工具之间的速度和便利的差别够大,它会改变你思考和使用工具的方式.自动地使一个函数带记忆仅需要消耗手动书写代码的1/100的时间.这和飞机与牛车的速度差别一样.说飞机只是更快的牛车就忽略了本质:量变如此巨大以致它也变成了实质的质变. 例如,有了自动的记忆术,就有可能为函数增加缓存行为而不必预先仔细考虑性能细节.记忆术这么简单以至于可

《高阶Perl》——1.8 当递归膨胀时

1.8 当递归膨胀时 有时一个问题很明显是递归,然后递归方案很没有效率.一个非常简单的例子是计算Fibonacci数.这是个想象的例子,但它的优点就是非常简单.我们将在3.7节看到一个更实际的例子. 1.8.1 Fibonacci数 Fibonacci数(Fibonacci number)以Leonardo of Pisa 命名,他的别名是Fibonacci,他在13世纪联系一个关于兔子的数学问题探讨了它们.起初,你有一对兔宝宝.兔宝宝一个月就发育成年了,下一个月它们生育了一对新的兔宝宝,这样有

《高阶Perl》——3.2 内联缓存

3.2 内联缓存 给一个函数添加缓存的最直接的方式就是给函数一个私有的散列.在这个例子里,可以使用一个数组代替散列,因为fib()的参数总是一个非负整数.但是一般需要使用一个散列,那么将会看到: ### Code Library: fib-cached # Compute the number of pairs of rabbits alive in month n { my %cache; sub fib { my ($month) = @_; unless (exists $cache{$m

《高阶Perl》——3.5 MEMOIZE模块

3.5 MEMOIZE模块 本书不是关于Perl模块的内部细节的,而是关于一些Memoize模块内部使用的并和稍后要做的事情有直接联系的技术,所以现在简短地介绍一下. Memoize得到一个函数名(或引用)作为它的参数.它制造一个新的函数,后者维护一个缓存并在其中查找它的参数.如果新的函数在缓存里找到了参数,就返回缓存的值:如果没找到,就调用原始函数,把返回的值保存入缓存,并把它返回给原始的主调者. 制造完这个新的函数,Memoize就把它装入Perl的符号表以代替原始的函数,那样当你认为你在调

《高阶Perl》——3.10 可供选择的记忆术

3.10 可供选择的记忆术 大多数纯函数提供一个缓存的机会.尽管乍一看纯函数很少,它们只以一定频率出现.纯函数特别普遍的地方是在排序中用做比较器函数. Perl内置的sort操作符是通用的,它可以把一列任何种类的数据以程序要求的任何次序排序.默认状态下,它把一列字符串以字母表次序排序,但是程序员可以任意提供一个比较器函数(comparator function),告诉Perl怎样重排sort的参数列表.比较器函数被反复调用,每次带有待排序列表中的两个不同元素,如果两个元素次序正确,就必须返回一个

《高阶Perl》——3.8 对象方法里的缓存

3.8 对象方法里的缓存 对象方法,它经常不理解地把缓存的值保存在独立的散列里.考虑一个投资银行写的程序里的Investor对象.该对象表现了银行的一个客户: package Investor; # Compute total amount currently invested sub total { my $self = shift; # ... complex computation performed here ... return $total; } 如果$total不会改变,就可以缓存

《高阶Perl》——1.7 HTML

1.7 HTML 我曾承诺递归对操作层次化定义的数据结构有用,我还用了文件系统作为一个例子.但它是个稍微特殊的数据结构的例子,因为通常认为数据结构是在内存里,而不是在磁盘上. 文件系统的树型结构让人联想到目录,每个目录都含有一列其他文件.任何拥有一些包含其他条目列表的领域都将含有树型结构.一个极好的例子是HTML数据. HTML数据是一系列元素和普通文本.每个元素含有一些内容,它们又是一系列更多的元素和更多的普通文本.这是个递归的描述,类似文件系统的描述,HTML文件的结构也类似于文件系统的结构