perl6 什么是 de-select 数组或列表元素的快速方法?

perl6 What is a quick way to de-select array or list elements?

要select perl6 中数组中的多个元素,很简单:只需使用索引列表即可:

> my @a = < a b c d e f g >;
> @a[ 1,3,5 ]
(b d f)

但是要取消 select 那些元素,我不得不使用 Set:

> say @a[ (@a.keys.Set (-) (1,3,5)).keys.sort ]
(a c e g)

我想知道是否有更简单的方法,因为我使用的数组通常很大?

sub infix:<not-at> ($elems, @not-ats) {
  my $at = 0;
  flat gather for @not-ats -> $not-at {
    when $at < $not-at { take $at++ xx $not-at - $at } 
    NEXT { $at++ }
    LAST { take $at++ xx $elems - $not-at - 1 }
  }
}

my @a = < a b c d e f g >;
say @a[ * not-at (1, 3, 5) ]; # (a c e g)

如果您知道它使用的每个 P6 结构,我认为运算符代码是不言自明的。如果有人希望得到除以下之外的解释,请在评论中告诉我。

我将从生成对 not-at 的调用的两个方面开始。

* 又名 Whatever

来自Whatever doc page:

When * is used in term position, that is, as an operand, in combination with most operators, the compiler will transform the expression into a closure of type WhateverCode

* 确实在上面用作操作数。在本例中,它是我刚刚创建的中缀 not-at 运算符的左参数(对应于 $elems 参数)。

下一个问题是,编译器会进行转换吗?编译器根据运算符是否具有显式 * 作为对应于 * 参数的参数来决定。如果我写 * 而不是 $elems 那么这将使 not-at 成为少数几个想要直接处理 * 并做任何它选择做的事情的操作员之一编译器会直接调用它。但我没有。我写了$elems。所以编译器会执行我接下来要描述的转换。

转换围绕封闭表达式构建了一个新的 WhateverCode,并将 Whatever 重写为 "it" 也就是主题 $_。所以在这种情况下它变成了这个:

* not-at (1,3,5)

进入这个:

{ $_ not-at (1,3,5) }

[...] 作为下标的作用

@a[...]中的[...]是一个Positional(array/list)下标。这强加了几个评估方面,其中两个在这里很重要:

  • "it" aka 主题 aka $_ 设置为 list/array.

  • 的长度
  • 如果下标的内容是 Callable,它就会被调用。如上所述生成的 WhateverCode 确实是 Callable 所以它被调用。

所以这个:

@a[ * not-at (1,3,5) ]

变成这样:

@a[ { $_ not-at [1,3,5] } ]

变成这样:

 @a[ { infix:not-at(7, [1,3,5]) } ]

还有一个选项:

my @a = < a b c d e f g >;
say @a[@a.keys.grep(none(1, 3, 5))];

但总而言之,数组并未针对此用例进行优化。它们针对处理单个元素或所有元素进行了优化,并且切片提供了一种快捷方式(积极地)按键选择多个元素。

如果您告诉我们底层用例,也许我们可以推荐更合适的数据结构。

鉴于索引器想要提取元素,我们可以通过将要排除的元素列表转换为要提取的元素范围列表来解决这个问题。即,给定:

1, 3, 5

我们会产生相当于:

0..0, 2..2, 4..4, 6..Inf

给定:

my @exclude = 1, 3, 5;

我们可以做到:

-1, |@exclude Z^..^ |@exclude, Inf

要分解它,将 (-1, 1, 3, 5) 压缩为 (1, 3, 5, Inf),但使用具有独占端点的范围运算符。对于给定的示例,这导致:

(-1^..^1 1^..^3 3^..^5 5^..^Inf)

这相当于我上面提到的范围。然后我们将其粘贴到索引器中:

my @a = <a b c d e f g>
my @exclude = 1, 3, 5;
say @a[-1, |@exclude Z^..^ |@exclude, Inf].flat

给出了想要的结果:

(a c e g)

这种方法是 O(n + m)。如果数组很长,它可能会很好地工作,但是要排除的东西的数量相对较少,因为它只产生索引所需的 Range 个对象,然后按范围进行索引优化得相对较好。

最后,如果外面的flat嫌麻烦,也可以移到里面:

@a[{ flat -1, |@exclude Z^..^ |@exclude, $_ }]

之所以有效,是因为块传递了 @a 中的元素数。

这对于大数组来说可能会很慢,但从逻辑上讲它更接近您要查找的内容:

my @a = <a b c d>;

say (@a ⊖ @a[0,1]).keys; # (c d)

这与您一开始提出的解决方案基本相同,使用集合差异,除了它是在整个数组而不是索引上使用它。此外,在某些情况下,您可能会直接使用该集合;这取决于你想做什么。

@raiphs 解决方案结合@Jonathan Worthington 的:

运算符对于巨大的数字和大型@not-ats 列表应该非常有效,因为它 returns 范围列表,它甚至懒惰地创建该范围列表。对于@not-ats,它支持包含和排除边界和无穷大的整数和范围。但必须是升序的。

$elems 可以是 Range 或 Int。它在 Jonathan Worthington 的解决方案中被解释为 .Int 来支持(但需要一个 .flat 将其应用于数组切片 - 惰性运算符的性能价格 - 这可以通过在第二行使用平面收集而不是惰性收集来改变)

@a[ (* not-at (1, 3, 5)).flat ];

或新增支持

@a[ (* not-at (1, 3^ .. 5, 8 .. 8, 10, 14 .. ^18, 19 .. *)).flat ];

当不立即对数组进行切片,而是对数组的一部分进行操作时,可以看到性能改进,最好是多线程。

sub infix:<not-at> ($elems, @not-ats) {
    lazy gather {
        my $at = 0;
        for @not-ats {                                               # iterate over @not-ats ranges
            my ($stop, $continue) = do given $_ {
                when Int            { succeed $_,       $_         } # 5
                when !.infinite     { succeed .int-bounds          } # 3..8 | 2^..8 | 3..^9 | 2^..^9
                when !.excludes-min { succeed .min,     $elems.Int } # 4..*
                default             { succeed .min + 1, $elems.Int } # 3^..*
            }
            take $at .. $stop - 1 if $at < $stop;                    # output Range before current $not-at range
            $at = $continue + 1;                                     # continue after current $not-at range
        }
        take $at .. $elems.Int - 1 if $at < $elems;                  # output Range with remaining elements
    }
}