功能Perl:Filter,Iterator

Tho*_*och 2 perl functional-programming

我必须编写Perl,尽管我对Java,Python和函数式语言更加熟悉.我想知道是否有一些惯用的方法来解析像这样的简单文件

# comment line - ignore

# ignore also empty lines
key1 = value
key2 = value1, value2, value3
Run Code Online (Sandbox Code Playgroud)

我想要一个函数,我通过文件的行传递迭代器,并返回从键到值列表的映射.但要实现功能性和结构化,我想:

  • 使用包装给定迭代器的过滤器并返回没有空行或注释行的迭代器
  • 所提到的过滤器应该在功能之外定义,以便其他功能可重用.
  • 使用给定行的另一个函数并返回键和值字符串的元组
  • 使用另一个函数将逗号分隔值分解为值列表.

什么是最现代,最惯用,最干净,功能最强的方式?代码的不同部分应该是可单独测试和可重用的.

作为参考,这是(快速入侵)我如何在Python中做到这一点:

re_is_comment_line = re.compile(r"^\s*#")
re_key_values = re.compile(r"^\s*(\w+)\s*=\s*(.*)$")
re_splitter = re.compile(r"\s*,\s*")
is_interesting_line = lambda line: not ("" == line or re_is_comment_line.match(line))
                                   and re_key_values.match(line)

def parse(lines):
    interesting_lines = ifilter(is_interesting_line, imap(strip, lines))
    key_values = imap(lambda x: re_key_values.match(x).groups(), interesting_lines)
    splitted_values = imap(lambda (k,v): (k, re_splitter.split(v)), key_values)
    return dict(splitted_values)
Run Code Online (Sandbox Code Playgroud)

amo*_*mon 5

你的Python的直接翻译将是

my $re_is_comment_line = qr/^\s*#/;
my $re_key_values      = qr/^\s*(\w+)\s*=\s*(.*)$/;
my $re_splitter        = qr/\s*,\s*/;
my $is_interesting_line= sub {
  my $_ = shift;
  length($_) and not /$re_is_comment_line/ and /$re_key_values/;
};

sub parse {
  my @lines = @_;
  my @interesting_lines = grep $is_interesting_line->($_), @lines;
  my @key_values = map [/$re_key_values/], @interesting_lines;
  my %splitted_values = map { $_->[0], [split $re_splitter, $_->[1]] } @key_values;
  return %splitted_values;
}
Run Code Online (Sandbox Code Playgroud)

差异是:

  • ifilter被调用grep,并且可以将表达式而不是块作为第一个参数.这些大致相当于一个lambda.当前项目在$_变量中给出.这同样适用于map.
  • Perl不会强调懒惰,也很少使用迭代器.有些情况需要这样做,但通常会立即评估整个列表.

在下一个示例中,将添加以下内容:

  • 正则表达式不必预编译,Perl非常适合正则表达式优化.
  • 我们使用的不是用正则表达式提取键/值split.它采用可选的第三个参数来限制结果片段的数量.
  • 整个map/ filter东西可以用一个表达式编写.这不会提高效率,但会强调数据流.从底部向上读取map-map-grep(实际上是从右到左,想到APL).

.

sub parse {
  my %splitted_values =
    map { $_->[0], [split /\s*,\s*/, $_->[1]] }
    map {[split /\s*=\s*/, $_, 2]}
    grep{ length and !/^\s*#/ and /^\s*\w+\s*=\s*\S/ }
    @_;
  return \%splitted_values; # returning a reference improves efficiency
}
Run Code Online (Sandbox Code Playgroud)

但我认为这里更优雅的解决方案是使用传统的循环:

sub parse {
  my %splitted_values;
  LINE: for (@_) {
    next LINE if !length or /^\s*#/;
    s/\A\s*|\s*\z//g; # Trimming the string—omitted in previous examples
    my ($key, $vals) = split /\s*=\s*/, $_, 2;
    defined $vals or next LINE; # check if $vals was assigned
    @{ $splitted_values{$key} } = split /\s*,\s*/, $vals; # Automatically create array in $splitted_values{$key}
  }
  return \%splitted_values
}
Run Code Online (Sandbox Code Playgroud)

如果我们决定传递一个文件句柄,那么循环将替换为

my $fh = shift;
LOOP: while (<$fh>) {
  chomp;
  ...;
}
Run Code Online (Sandbox Code Playgroud)

哪个会使用实际的迭代器.

您现在可以添加函数参数,但只有在优化灵活性而不是其他任何内容时才能执行此操作.我已在第一个示例中使用了代码引用.您可以使用$code->(@args)语法调用它们.

use Carp; # Error handling for writing APIs
sub parse {
  my $args = shift;
  my $interesting  = $args->{interesting}   or croak qq("interesting" callback required);
  my $kv_splitter  = $args->{kv_splitter}   or croak qq("kv_splitter" callback required);
  my $val_transform= $args->{val_transform} || sub { $_[0] }; # identity by default

  my %splitted_values;
  LINE: for (@_) {
    next LINE unless $interesting->($_);
    s/\A\s*|\s*\z//g;
    my ($key, $vals) = $kv_splitter->($_);
    defined $vals or next LINE;
    $splitted_values{$key} = $val_transform->($vals);
  }
  return \%splitted_values;
}
Run Code Online (Sandbox Code Playgroud)

然后可以这样调用

my $data = parse {
  interesting   => sub { length($_[0]) and not $_[0] =~ /^\s*#/ },
  kv_splitter   => sub { split /\s*=\s*/, $_[0], 2 },
  val_transform => sub { [ split /\s*,\s*/, $_[0] ] }, # returns anonymous arrayref
}, @lines;
Run Code Online (Sandbox Code Playgroud)