如何在ruby中执行惯用的非递归展平?

nas*_*orn 4 ruby

我有一个返回数组数组的方法.为方便起见,我在集合上使用collect来将它们聚集在一起.

arr = collection.collect {|item| item.get_array_of_arrays}
Run Code Online (Sandbox Code Playgroud)

现在我想拥有一个包含所有数组的数组.当然我可以遍历数组并使用+运算符来做到这一点.

newarr = []    
arr.each {|item| newarr += item}
Run Code Online (Sandbox Code Playgroud)

但这有点难看,还有更好的方法吗?

Jör*_*tag 28

有一种在Ruby中展平数组的方法Array#flatten:

newarr = arr.flatten(1)
Run Code Online (Sandbox Code Playgroud)

从你的描述看起来它实际上看起来你不再关心arr了,所以没有必要保留旧的价值arr,我们可以修改它:

arr.flatten!(1)
Run Code Online (Sandbox Code Playgroud)

(在Ruby中有一条规则说,如果你有两个方法基本上做同样的事情,但是有一个方法以一种令人惊讶的方式做到了,你可以将该方法命名为与其他方法相同但最后有一个感叹号在这种情况下,两种方法都会压平数组,但带有感叹号的版本会通过销毁原始数组来实现.)

然而,虽然在这种特殊情况下实际上有一种方法可以完全满足你的需要,但是在你的代码中有一个更通用的原则:你有一系列的东西,你迭代它并尝试"减少"它变成一件事.在这种情况下,很难看到,因为你从数组开始,最终得到一个数组.但是通过改变代码中的一些小细节,它突然变得非常明显:

sum = 0
arr.each {|item| sum += item } # assume arr is an array of numbers
Run Code Online (Sandbox Code Playgroud)

这是完全相同的模式.

你要做的事情被称为类别理论中的变形,数学折叠,函数编程的减少,inject:into:在Smalltalk中,并且在Ruby中由Enumerable#inject它的别名Enumerable#reduce(或在本例中实际上)实现.Array#injectArray#reduce

很容易发现:无论何时在循环外部初始化累加器变量然后分配给它或修改它在循环的每次迭代期间引用的对象,那么你有一个案例reduce.

在这种特殊情况下,您的累加器是newarr,并且操作是向其添加数组.

所以,你的循环可以更像惯用地重写:

newarr = arr.reduce(:+)
Run Code Online (Sandbox Code Playgroud)

经验丰富的Rubyist当然会马上看到这一点.然而,通过遵循一些简单的重构步骤,即使是新手最终也会到达那里,可能类似于:

首先,你意识到它实际上一个折叠:

newarr = arr.reduce([]) {|acc, el| acc += el }
Run Code Online (Sandbox Code Playgroud)

接下来,你就会意识到,分配给acc是完全没有必要的,因为reduce内容覆盖acc 反正每次迭代的结果值:

newarr = arr.reduce([]) {|acc, el| acc + el }
Run Code Online (Sandbox Code Playgroud)

第三,没有必要注入一个空数组作为第一次迭代的起始值,因为所有元素arr都已经是数组:

newarr = arr.reduce {|acc, el| acc + el }
Run Code Online (Sandbox Code Playgroud)

当然,这可以通过以下方式进一步简化Symbol#to_proc:

newarr = arr.reduce(&:+)
Run Code Online (Sandbox Code Playgroud)

实际上,我们不需要Symbol#to_proc这里,因为reduce并且inject已经接受了操作的符号参数:

newarr = arr.reduce(:+)
Run Code Online (Sandbox Code Playgroud)

这确实一种普遍的模式.如果您还记得sum上面的示例,它将如下所示:

sum = arr.reduce(:+)
Run Code Online (Sandbox Code Playgroud)

除变量名外,代码没有变化.

  • @nasmorn:这就是我将递归级别限制为"1"的原因.我确实在我的答案中测试了每一行代码,并且它们都给出了与原始示例和@Simone Carletti完全相同的结果. (2认同)