递归生成器 - 手动zip与运算符

时间:2017-06-29 09:59:25

标签: perl6

以下是Charles C Pinter的“摘要代数书”中的练习5.F.2:

  

{e, a, b, b^2, b^3, ab, ab^2, ab^3}成为其中的a^2 = e组   生成器满足b^4 = eba = ab^3G。写表   G。 (sub generate(%eqs, $s) { my @results = (); for %eqs.kv -> $key, $val { if $s ~~ /$key/ { @results.push($s.subst(/$key/, $val)); } if $s ~~ /$val/ { @results.push($s.subst(/$val/, $key)); } } for @results -> $result { take $result; } my @arrs = @results.map({ gather generate(%eqs, $_) }); my $i = 0; while (1) { for @arrs -> @arr { take @arr[$i]; } $i++; } } sub table(@G, %eqs) { printf " |"; for @G -> $y { printf "%-5s|", $y; }; say ''; printf "-----|"; for @G -> $y { printf "-----|"; }; say ''; for @G -> $x { printf "%-5s|", $x; for @G -> $y { my $result = (gather generate(%eqs, "$x$y")).first(* (elem) @G); printf "%-5s|", $result; } say '' } } # ---------------------------------------------------------------------- # Pinter 5.F.2 my @G = <e a b bb bbb ab abb abbb>; my %eqs = <aa e bbbb e ba abbb>; %eqs<e> = ''; table @G, %eqs; 称为二面体组D4。)

这是一个小的Perl 6程序,它提供了一个解决方案:

generate

以下是结果表的样子:

enter image description here

让我们关注my @arrs = @results.map({ gather generate(%eqs, $_) }); my $i = 0; while (1) { for @arrs -> @arr { take @arr[$i]; } $i++; }

中的这些特定行
generate

@results中的每个项目进行zip的递归调用。然后我们在结果序列上有效地执行手动“zip”。但是,Perl 6有Zfor ([Z] @results.map({ gather generate(%eqs, $_) })).flat -> $elt { take $elt; } 运算符。

而不是以上几行,我想做这样的事情:

generate

所以这是使用Z的完整sub generate(%eqs, $s) { my @results = (); for %eqs.kv -> $key, $val { if $s ~~ /$key/ { @results.push($s.subst(/$key/, $val)); } if $s ~~ /$val/ { @results.push($s.subst(/$val/, $key)); } } for @results -> $result { take $result; } for ([Z] @results.map({ gather generate(%eqs, $_) })).flat -> $elt { take $elt; } }

Z

生成的generate版本的问题是它挂起......

enter image description here

所以,我的问题是,有没有办法用Z来写{e, a, b, b^2, b^3, ab, ab^2, ab^3}

除了这个核心问题,请随意分享探索和展示Perl 6的练习的替代解决方案。

另一个例子,这是练习5.F.3来自同一本书:

  

设G为a^4 = e组   生成器满足a^2 = b^2ba = ab^3G。写下来   generate的表格。 (G称为四元数组。)

上面显示表格的程序:

enter image description here

顺便说一下,这个程序是从C#中的版本转换而来的。以下是 static IEnumerable<string> generate(Dictionary<string,string> eqs, string s) { var results = new List<string>(); foreach (var elt in eqs) { if (new Regex(elt.Key).IsMatch(s)) results.Add(new Regex(elt.Key).Replace(s, elt.Value, 1)); if (new Regex(elt.Value).IsMatch(s)) results.Add(new Regex(elt.Value).Replace(s, elt.Key, 1)); } foreach (var result in results) yield return result; foreach (var elt in ZipMany(results.Select(elt => generate(eqs, elt)), elts => elts).SelectMany(elts => elts)) yield return elt; } 使用LINQ和ZipMany Eric Lippertlink版本的内容。

<select name="car">
  <option value="volvo">Volvo</option>
  <option value="saab">Saab</option>
  <option value="mercedes">Mercedes</option>
  <option value="audi">Audi</option>
</select>

整个C#计划:Bootstrapping muliple components in Angular2

4 个答案:

答案 0 :(得分:8)

为什么使用zip不起作用

您的代码假定[Z](“使用zip运算符减少”)可用于获取列表列表的transpose

不幸的是,在一般情况下,不起作用 它“通常”有效,但在一个边缘情况下中断:即,列表列表是一个完全一个列表的列表。观察:

my @a = <a b c>, <1 2 3>, <X Y Z>; put [Z~] @a;  # a1X b2Y c3Z
my @a = <a b c>, <1 2 3>;          put [Z~] @a;  # a1 b2 c3
my @a = <a b c>,;                  put [Z~] @a;  # abc
my @a;                             put [Z~] @a;  # 

在前两个示例(3和2子列表)中,您可以看到@a的转置返回正常。第四个例子(0个子列表)也是正确的 但是第三个例子(1个子列表)没有按照人们的预期打印a b c,即在这种情况下它没有返回@a的转置,而是(似乎)转置@a[0]

可悲的是,这不是一个Rakudo错误(在这种情况下它可以简单地修复),而是两个Perl 6设计决策的不可预见的交互,即:

  • reduce元运算符[ ] 通过使用一个参数(所述元素)调用应用于它的运算符来处理带有单个元素的输入列表。
    如果您想知道,可以通过调用其函数对象来调用仅使用一个参数的中缀运算符:&infix:<Z>( <a b c>, )
  • zip运算符Z和函数zip (与其他接受嵌套列表的内置函数一样)遵循所谓的“单参数规则” - 即它的签名使用single-argument slurpy parameter。这意味着当使用单个参数调用它时,它将下降到它并考虑其元素要使用的实际参数。 (另见Slurpy conventions。)
    因此zip(<a b c>,)被视为zip("a", "b", "c")

这两个功能在许多其他情况下提供了一些不错的便利,但在这种情况下,他们的互动令人遗憾地构成陷阱。

如何使其与zip

一起使用

您可以检查@arrs的元素数量,以及特殊情况下的“确切1个子列表”案例:

my @arrs = @results.map({ gather generate(%eqs, $_) });

if @arrs.elems == 1 {
    .take for @arrs[0][];
}
else {
    .take for flat [Z] @arrs
}

[]是一个“zen slice” - 它返回列表不变,但没有父数据包装它的项容器。这是必需的,因为for循环会将包装在项容器中的任何内容视为单个项目,并且只进行一次迭代。

当然,这个if-else解决方案不是很优雅,这可能会否定你首先尝试使用zip的理由。

如何更优雅地编写代码没有 zip

请参阅Christoph's answer

答案 1 :(得分:6)

有可能使用Z,但对于我可怜的小脑,拉链递归生成的懒惰列表太多了。

相反,我做了一些其他的简化:

sub generate($s, %eqs) {
    take $s;

    # the given equations normalize the string, ie there's no need to apply
    # the inverse relation
    for %eqs.kv -> $k, $v {
        # make copy of $s so we can use s/// instead of .subst
        my $t = $s;
        generate $t, %eqs
            if $t ~~ s/$k/$v/;
    }
}

sub table(@G, %eqs) {
    # compute the set only once instead of implicitly on each call to (elem)
    my $G = set @G;

    # some code golfing
    put ['', |@G]>>.fmt('%-5s|').join;
    put '-----|' x @G + 1;

    for @G -> $x {
        printf '%-5s|', $x;

        for @G -> $y {
            printf '%-5s|', (gather generate("$x$y", %eqs)).first(* (elem) $G);
        }

        put '';
    }    
}

my @G = <e a b bb bbb ab abb abbb>;

# use double brackets so we can have empty strings
my %eqs = <<aa e   bbbb e   ba abbb   e ''>>;

table @G, %eqs;

这是一个generate的紧凑重写,它进行双向替换,仍然没有显式的zip:

sub generate($s, %eqs) {
    my @results = do for |%eqs.pairs, |%eqs.antipairs -> (:$key, :$value) {
        take $s.subst($key, $value) if $s ~~ /$key/;
    }

    my @seqs = @results.map: { gather generate($_, %eqs) }
    for 0..* -> $i { take .[$i] for @seqs }
}

答案 2 :(得分:5)

以下是使用smls演示的方法的generate版本:

sub generate(%eqs, $s)
{
    my @results = ();

    for %eqs.kv -> $key, $val {
        if $s ~~ /$key/ { @results.push($s.subst(/$key/, $val)); }
        if $s ~~ /$val/ { @results.push($s.subst(/$val/, $key)); }
    }

    for @results -> $result { take $result; }

    my @arrs = @results.map({ gather generate(%eqs, $_) });

    if @arrs.elems == 1 { .take for @arrs[0][]; }
    else { .take for flat [Z] @arrs; }
}

我测试了它,它适用于练习2和3。

正如smls在他的回答中提到的那样,当给定的数组数组只包含一个数组时,zip没有达到我们所期望的那样。所以,让我们制作一个版本zip 使用一个或多个数组:

sub zip-many (@arrs)
{
    if @arrs.elems == 1 { .take for @arrs[0][];     }
    else                { .take for flat [Z] @arrs; }
}

现在,generatezip-many而言:

sub generate(%eqs, $s)
{
    my @results = ();

    for %eqs.kv -> $key, $val {
        if $s ~~ /$key/ { @results.push($s.subst(/$key/, $val)); }
        if $s ~~ /$val/ { @results.push($s.subst(/$val/, $key)); }
    }

    for @results -> $result { take $result; }

    zip-many @results.map({ gather generate(%eqs, $_) });
}

看起来很不错。

谢谢smls

smls在下面的评论中建议zip-many不要调用take,将其留给generate。我们也将flatzip-many移到generate

瘦身zip-many

sub zip-many (@arrs) { @arrs == 1 ?? @arrs[0][] !! [Z] @arrs }

generate同时使用它:

sub generate(%eqs, $s)
{
    my @results;

    for %eqs.kv -> $key, $val {
        if $s ~~ /$key/ { @results.push($s.subst(/$key/, $val)); }
        if $s ~~ /$val/ { @results.push($s.subst(/$val/, $key)); }
    }

    .take for @results;

    .take for flat zip-many @results.map({ gather generate(%eqs, $_) });
}

答案 3 :(得分:0)

分别测试键和值似乎有点傻;你的字符串不是真正的正则表达式,所以代码中的任何地方都不需要//

sub generate($s, @eqs) {
    my @results = do for @eqs.kv -> $i, $equation {
        take $s.subst($equation, @eqs[ $i +^ 1 ]) if $s.index: $equation
    }

    my @seqs = @results.map: { gather generate($_, @eqs) }
    for 0..* -> $i { take .[$i] for @seqs }
}

显然,对于此版本的generate,您必须重写table才能使用@eqs代替%eqs