在Ruby中,是否存在结合“选择”和“映射”的Array方法?


96

我有一个包含一些字符串值的Ruby数组。我需要:

  1. 查找与某些谓词匹配的所有元素
  2. 通过转换运行匹配的元素
  3. 以数组形式返回结果

现在,我的解决方案如下所示:

def example
  matchingLines = @lines.select{ |line| ... }
  results = matchingLines.map{ |line| ... }
  return results.uniq.sort
end

是否存在将select和map组合为单个逻辑语句的Array或Enumerable方法?


5
目前尚无方法,但建议将其添加到Ruby:bugs.ruby-lang.org/issues/5663
stefankolb 2015年

Enumerable#grep方法完全符合要求,并且已经在Ruby中使用了十多年。它带有一个谓词参数和一个转换块。@hirolau给出此问题的唯一正确答案。
inopinatus

2
Ruby 2.7正是filter_map为此目的而引入的。更多信息在这里
SRack

Answers:


115

我通常将mapcompact以及选择标准一起用作后缀ifcompact摆脱指甲。

jruby-1.5.0 > [1,1,1,2,3,4].map{|n| n*3 if n==1}    
 => [3, 3, 3, nil, nil, nil] 


jruby-1.5.0 > [1,1,1,2,3,4].map{|n| n*3 if n==1}.compact
 => [3, 3, 3] 

1
啊哈,我试图弄清楚如何忽略地图块返回的nil。谢谢!
塞斯·佩特里·约翰逊

没问题,我喜欢紧凑型。它毫不客气地坐在那里做它的工作。对于简单的选择标准,我也更喜欢将此方法链接到可枚举的函数,因为它非常声明。
杰德·施耐德

4
我不能确定,如果map+ compact会真正执行优于inject和张贴了我的基准测试结果的相关主题:stackoverflow.com/questions/310426/list-comprehension-in-ruby/...
knuton

3
这将删除所有nil,包括原始nil和不符合条件的nil。所以要
当心

1
它并不完全消除链mapselect,它只是compact一种特殊情况reject,关于尼尔斯和执行因已在C.直接实现对作品有所好转
乔Atzberger

53

您可以使用reduce此方法,只需一遍:

[1,1,1,2,3,4].reduce([]) { |a, n| a.push(n*3) if n==1; a }
=> [3, 3, 3] 

换句话说,将状态初始化为您想要的状态(在本例中为空列表,以填充:)[],然后始终确保对原始列表中的每个元素进行修改后返回此值(在本例中,为修改后的元素)推送到列表)。

这是最有效的方法,因为它只循环遍历列表一次(map+ selectcompact需要两次)。

在您的情况下:

def example
  results = @lines.reduce([]) do |lines, line|
    lines.push( ...(line) ) if ...
    lines
  end
  return results.uniq.sort
end

20
each_with_object做出一点更有意义?您不必在每次块迭代结束时都返回数组。你可以简单地做my_array.each_with_object([]) { |i, a| a << i if i.condition }
henrebotha'8

@henrebotha也许是的。我从功能背景的,这就是为什么我发现reduce第一😊
亚当·林德伯格

35

Ruby 2.7以上

现在有!

Ruby 2.7即将推出 filter_map为此目的的。它是惯用语言和高性能,我希望它很快会成为标准。

例如:

numbers = [1, 2, 5, 8, 10, 13]
enum.filter_map { |i| i * 2 if i.even? }
# => [4, 16, 20]

这是一个很好的主题读物

希望对某人有用!


1
不管我多久升级一次,下一个版本中总会提供一个很棒的功能。
凌晨

真好 一个问题可能是因为filterselectfind_all是同义的,就像mapcollect是,它可能很难记住这个方法的名称。它是filter_mapselect_collectfind_all_mapfilter_collect
埃里克·杜米尼尔

19

解决此问题的另一种方法是使用new(相对于此问题)Enumerator::Lazy

def example
  @lines.lazy
        .select { |line| line.property == requirement }
        .map    { |line| transforming_method(line) }
        .uniq
        .sort
end

.lazy方法返回一个惰性枚举器。在惰性枚举器上调用.select或会.map返回另一个惰性枚举器。只有调用一次.uniq,它实际上会强制枚举器并返回一个数组。所以有效的发生就是您.select和您的.map呼叫被合并为一个-您只需重复@lines一次即可完成.select.map

我的直觉是,亚当的reduce方法会更快一些,但我认为这更具可读性。


这样做的主要结果是,没有为每个后续方法调用创建中间数组对象。在正常@lines.select.map情况下,select返回一个数组,然后通过对其进行修改map,再次返回一个数组。相比之下,惰性求值仅创建一次数组。当您的初始收集对象很大时,这很有用。它还使您能够使用无限枚举器-例如random_number_generator.lazy.select(&:odd?).take(10)


4
给每个人自己。通过这种解决方案,我可以浏览一下方法名称,并立即知道我将要转换输入数据的子集,使其唯一并对其进行排序。reduce作为“尽一切所能”的转变对我来说总是很混乱。
henrebotha

2
@henrebotha:如果我误解了您的意思,请原谅我,但这是非常重要的一点:说“您只重复一次才能完成两者和” 是不正确的。使用并不意味着在惰性枚举器上进行的链式操作将“折叠”为一个迭代。这是对集合的惰性评估和链接操作的普遍误解。(您可以在第一个示例中的and 块的开头添加一条语句来进行测试。您会发现它们打印的行数相同)@lines.select.map.lazyputsselectmap
pje

1
@henrebotha:如果将.lazy其删除,它将打印相同的次数。这就是我的意思— 在懒惰和热切的版本中,您的map块和您的select块执行了相同的次数。懒惰的版本不“结合你.select.map电话”
司法警察

1
@pje:实际上 lazy将它们组合在一起,因为失败select条件的元素不会传递给map。换句话说:预谋lazy大致相当于替换selectmap利用单个reduce([]),以及“智能地”作出select的块的一个先决条件列入reduce的结果。
henrebotha

1
@henrebotha:我认为这通常是对延迟评估的误导类推,因为延迟不会改变该算法的时间复杂度。这就是我的观点:在每种情况下,懒惰的select-then-map总是会执行与其渴望的版本相同数量的计算。它并没有加快任何速度,它只是改变了每次迭代的执行顺序-链中的最后一个函数根据需要从先前函数中以相反的顺序“拉”值。
pje

13

如果您select可以使用case运算符(===),grep则是一个不错的选择:

p [1,2,'not_a_number',3].grep(Integer){|x| -x } #=> [-1, -2, -3]

p ['1','2','not_a_number','3'].grep(/\D/, &:upcase) #=> ["NOT_A_NUMBER"]

如果我们需要更复杂的逻辑,则可以创建lambda:

my_favourite_numbers = [1,4,6]

is_a_favourite_number = -> x { my_favourite_numbers.include? x }

make_awesome = -> x { "***#{x}***" }

my_data = [1,2,3,4]

p my_data.grep(is_a_favourite_number, &make_awesome) #=> ["***1***", "***4***"]

这不是替代方案,而是对问题的唯一正确答案。
inopinatus

@inopinatus:不再。但是,这仍然是一个很好的答案。我不记得看到grep有一个块。
埃里克·杜米尼尔

8

我不确定是否有一个。可枚举模块,它添加selectmap没有显示一个。

您需要将两个区块传递给 select_and_transform方法,这有点不直观。

显然,您可以将它们链接在一起,这更具可读性:

transformed_list = lines.select{|line| ...}.map{|line| ... }

3

简单答案:

如果你有N条记录,并且要selectmap根据病情再

records.map { |record| record.attribute if condition }.compact

在这里,属性是您想要从记录和条件中获得的任何条件,可以进行任何检查。

紧凑是冲洗如果条件产生的不必要的零


1
您也可以将其与除非条件一起使用。正如我的朋友问的。
Sk。Irfan

2

不,但是您可以这样做:

lines.map { |line| do_some_action if check_some_property  }.reject(&:nil?)

甚至更好:

lines.inject([]) { |all, line| all << line if check_some_property; all }

14
reject(&:nil?)基本上与相同compact
约尔格W¯¯米塔格

是的,所以注入方法更好。
丹尼尔·奥哈拉

2

我认为这种方式更具可读性,因为拆分了过滤条件和映射值,同时又清楚地知道动作已连接:

results = @lines.select { |line|
  line.should_include?
}.map do |line|
  line.value_to_map
end

并且,在您的特定情况下,请result一起消除该变量:

def example
  @lines.select { |line|
    line.should_include?
  }.map { |line|
    line.value_to_map
  }.uniq.sort
end

1
def example
  @lines.select {|line| ... }.map {|line| ... }.uniq.sort
end

在Ruby 1.9和1.8.7中,您也可以通过不向其传递块来链接和包装迭代器:

enum.select.map {|bla| ... }

但是在这种情况下实际上是不可能的,因为块的类型返回select和的值map不匹配。对于这样的事情更有意义:

enum.inject.with_index {|(acc, el), idx| ... }

AFAICS,您能做的最好的就是第一个例子。

这是一个小例子:

%w[a b 1 2 c d].map.select {|e| if /[0-9]/ =~ e then false else e.upcase end }
# => ["a", "b", "c", "d"]

%w[a b 1 2 c d].select.map {|e| if /[0-9]/ =~ e then false else e.upcase end }
# => ["A", "B", false, false, "C", "D"]

但是你真正想要的是["A", "B", "C", "D"]


昨晚我在“ Ruby中的方法链接”上做了一个非常简短的网络搜索,似乎它没有得到很好的支持。星期四,我可能应该尝试过...而且,为什么您说block参数的类型不匹配?在我的示例中,两个块都从我的数组中获取一行文本,对吗?
塞斯·佩特里·约翰逊

@Seth Petry-Johnson:是的,很抱歉,我的意思是返回值。select返回一个布尔型值,该值决定是否保留该元素,并map返回转换后的值。转换后的值本身可能将是真实的,因此将选择所有元素。
约尔格W¯¯米塔格

1

您应该尝试使用在其中添加了方法的库Rearmed RubyEnumerable#select_map。这里是一个例子:

items = [{version: "1.1"}, {version: nil}, {version: false}]

items.select_map{|x| x[:version]} #=> [{version: "1.1"}]
# or without enumerable monkey patch
Rearmed.select_map(items){|x| x[:version]}

select_map这个库中的代码只是根据select { |i| ... }.map { |i| ... }上述许多答案实施了相同的策略。
乔丹·锡金

1

如果不想创建两个不同的数组,可以使用compact!但要小心。

array = [1,1,1,2,3,4]
new_array = map{|n| n*3 if n==1}
new_array.compact!

有趣的是,compact!就地清除了nil。compact!如果有更改,则返回值是相同的数组,如果没有nil,则返回nil。

array = [1,1,1,2,3,4]
new_array = map{|n| n*3 if n==1}.tap { |array| array.compact! }

将是一个班轮。


0

您的版本:

def example
  matchingLines = @lines.select{ |line| ... }
  results = matchingLines.map{ |line| ... }
  return results.uniq.sort
end

我的版本:

def example
  results = {}
  @lines.each{ |line| results[line] = true if ... }
  return results.keys.sort
end

这将进行1次迭代(排序除外),并具有保持唯一性的额外好处(如果您不关心uniq,则只需将结果制成数组并 results.push(line) if ...


-1

这是一个例子。它与您的问题不同,但可能是您想要的,或者可以为您提供解决方案的线索:

def example
  lines.each do |x|
    new_value = do_transform(x)
    if new_value == some_thing
      return new_value    # here jump out example method directly.
    else
      next                # continue next iterate.
    end
  end
end
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.