【问题标题】:functional Perl: Filter, Iterator函数式 Perl:过滤器、迭代器
【发布时间】:2013-04-30 10:36:32
【问题描述】:

我必须编写 Perl,尽管我更熟悉 Java、Python 和函数式语言。我想知道是否有一些惯用的方法来解析一个简单的文件,比如

# comment line - ignore

# ignore also empty lines
key1 = value
key2 = value1, value2, value3

我想要一个函数,我在文件的行上传递一个迭代器,并返回一个从键到值列表的映射。但为了功能和结构化,我想:

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

什么是最现代、最惯用、最干净且仍然实用的方法?代码的不同部分应分别可测试和可重用。

作为参考,这里是(快速破解)我可以如何在 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)

【问题讨论】:

  • 在 Perl 中进行真正的函数式编程具有挑战性。对于这样的文件读取,我会坚持使用更迭代的方法。

标签: perl functional-programming


【解决方案1】:

你的 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;
}

区别是:

  • 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
}

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

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
}

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

my $fh = shift;
LOOP: while (<$fh>) {
  chomp;
  ...;
}

这将使用一个实际的迭代器。

您现在可以添加函数参数,但仅当您正在优化灵活性并且什么都没有时才这样做。我已经在第一个示例中使用了代码参考。您可以使用$code-&gt;(@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;
}

然后可以这样调用

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;

【讨论】:

  • 不错。在值拆分中,我认为您可以在逗号之前留下\s*,因为该空间应该被先前的kv拆分和右侧的\s*吃掉。
  • @goldilocks 不,我不能。例如。考虑key[ = ]foo(, )bar( ,)baz(括号标记值拆分删除的内容,括号标记kv-split)
  • 噢! -- 反正大崩溃。
【解决方案2】:

我认为最现代的方法在于利用 CPAN 模块。在您的示例中,Config::Properties 可能会有所帮助:

use strict;
use warnings;
use Config::Properties;

my $config = Config::Properties->new(file => 'example.properties') or die $!;
my $value = $config->getProperty('key');

【讨论】:

    【解决方案3】:

    正如@collapsar 链接的帖子中所指出的,Higher-Order Perl 是探索 Perl 中的函数式技术的绝佳读物。

    这是一个符合要点的示例:

    use strict;
    use warnings;
    use Data::Dumper;
    
    my @filt_rx = ( qr{^\s*\#},
                    qr{^[\r\n]+$} );
    my $kv_rx = qr{^\s*(\w+)\s*=\s*([^\r\n]*)};
    my $spl_rx = qr{\s*,\s*};
    
    my $iterator = sub {
        my ($fh) = @_;
        return sub {
            my $line = readline($fh);
            return $line;
        };
    };
    my $filter = sub {
        my ($it,@r) = @_;
        return sub {
            my $line;
            do {
                $line = $it->();
            } while (  defined $line
                    && grep { $line =~ m/$_/} @r );
            return $line;
        };
    };
    my $kv = sub {
        my ($line,$rx) = @_;
        return ($line =~ m/$rx/);
    };
    my $spl = sub {
        my ($values,$rx) = @_;
        return split $rx, $values;
    };
    
    my $it = $iterator->( \*DATA );
    my $f = $filter->($it,@filt_rx);
    
    my %map;
    while ( my $line = $f->() ) {
        my ($k,$v) = $kv->($line,$kv_rx);
        $map{$k} = [ $spl->($v,$spl_rx) ];
    }
    print Dumper \%map;
    
    __DATA__
    # comment line - ignore
    
    # ignore also empty lines
    key1 = value
    key2 = value1, value2, value3
    

    它在提供的输入上产生以下哈希:

    $VAR1 = {
              'key2' => [
                          'value1',
                          'value2',
                          'value3'
                        ],
              'key1' => [
                          'value'
                        ]
            };
    

    【讨论】:

    • 非常好的练习来展示如何实现迭代器。 (但是,readline 应该强制在标量上下文中,否则不能保证它是一个迭代器。请注意,选择 $kv_rx 会隐式地从每一行中删除换行符。可以说这样做会更好显式)
    【解决方案4】:

    您可能对this SO questionthis one 感兴趣。

    以下代码是一个独立的 perl 脚本,旨在让您了解如何在 perl 中实现(仅部分采用函数式风格;如果您不反感看到特定的编码风格和/或语言结构,我可以稍微改进一下解决方案)。

    Miguel Prz 是对的,在大多数情况下,您会搜索 CPAN 以找到符合您要求的解决方案。

    my (
          $is_interesting_line
        , $re_is_comment_line
        , $re_key_values
        , $re_splitter
    );
    
    $re_is_comment_line = qr(^\s*#);
    $re_key_values      = qr(^\s*(\w+)\s*=\s*(.*)$);
    $re_splitter        = qr(\s*,\s*);
    $is_interesting_line = sub {
            my $line = shift;
            return (
                    (!(
                            !defined($line)
                         || ($line eq '')
                    ))
                &&  ($line =~ /$re_key_values/)
            );
        };
    
    sub strip {
        my $line = shift;
        # your implementation goes here
        return $line;
    }
    sub parse {
        my @lines = @_;
        #
        my (
              $dict
            , $interesting_lines
            , $k
            , $v
        );
        #
        @$interesting_lines =
            grep {
                    &{$is_interesting_line} ( $_ );
                } ( map { strip($_); } @lines )
        ;
    
        $dict = {};
        map {
            if ($_ =~ /$re_key_values/) {
                ($k, $v) = ($1, [split(/$re_splitter/, $2)]);
                $$dict{$k} = $v;
            }
        } @$interesting_lines;
    
        return $dict;
    } # parse
    
    #
    # sample execution goes here
    #    
    my $parse =<<EOL;
    # comment
    what = is, this, you, wonder
    it = is, perl
    EOL
    
    parse ( split (/[\r\n]+/, $parse) );
    

    【讨论】:

      猜你喜欢
      • 2020-09-28
      • 2015-02-23
      • 2012-06-19
      • 2015-10-04
      • 1970-01-01
      • 2014-10-27
      • 1970-01-01
      • 1970-01-01
      • 2021-02-26
      相关资源
      最近更新 更多