Skip over iteration in Enumerable#collect
Asked Answered
C

6

60
(1..4).collect do |x|
  next if x == 3
  x + 1
end # => [2, 3, nil, 5]
    # desired => [2, 3, 5]

If the condition for next is met, collect puts nil in the array, whereas what I'm trying to do is put no element in the returned array if the condition is met. Is this possible without calling delete_if { |x| x == nil } on the returned array?

My code excerpt is heavily abstracted, so looking for a general solution to the problem.

Convince answered 1/3, 2011 at 8:38 Comment(1)
There's a real quick, built to purpose solution to this introduced in 2.7: filter_map, more info here.Coble
C
11

In Ruby 2.7+, it’s possible to use filter_map for this exact purpose. From the docs:

Returns an array containing truthy elements returned by the block.

(0..9).filter_map {|i| i * 2 if i.even? }   #=> [0, 4, 8, 12, 16]
{foo: 0, bar: 1, baz: 2}.filter_map {|key, value| key if value.even? }  #=> [:foo, :baz]

For the example in the question: (1..4).filter_map { |x| x + 1 unless x == 3 }.

See this post for comparison with alternative methods, including benchmarks.

Coble answered 2/8, 2019 at 9:35 Comment(1)
Great find :) It also works with next as tried by the OP: numbers.filter_map { |i| next if i.odd?; i * 2 }Bile
B
80

There is method Enumerable#reject which serves just the purpose:

(1..4).reject{|x| x == 3}.collect{|x| x + 1}

The practice of directly using an output of one method as an input of another is called method chaining and is very common in Ruby.

BTW, map (or collect) is used for direct mapping of input enumerable to the output one. If you need to output different number of elements, chances are that you need another method of Enumerable.

Edit: If you are bothered by the fact that some of the elements are iterated twice, you can use less elegant solution based on inject (or its similar method named each_with_object):

(1..4).each_with_object([]){|x,a| a << x + 1 unless x == 3}
Bilski answered 1/3, 2011 at 9:11 Comment(2)
This is the slowest answer (not by much though), but is certainly the cleanest. I just wish there was a way to do this with collect, as I would've expected calling next to have returned absolutely nothing, rather than "nothing" (aka nil).Convince
If it's slower than any other approach, Ruby's optimizer could use some work don't you think?Fardel
M
49

I would simply call .compact on the resultant array, which removes any instances of nil in an array. If you'd like it to modify the existing array (no reason not to), use .compact!:

(1..4).collect do |x|
  next if x == 3
  x
end.compact!
Maura answered 1/3, 2011 at 8:42 Comment(8)
I did a quick benchmark, for interest, of the four solutions suggested so far: collect+compact, collect+compact!, reject+collect and building a results array as you go. In MRI 1.9.1, at least, collect+compact! is the speed winner by a narrow margin, with collect+compact and reject+collect pretty close behind. Building the results array is about twice as slow as those.Friarbird
@glenn: Would you mind including the following to your benchmark: (1..4).inject([]){|a,x| x == 3 ? a : a.push(x + 1)}? Thanks.Borszcz
Sure. That's the slowest one yet, albeit only slightly slower than building the results array. I also added a version tweaked like this: a.inject([]){|aa,x| aa << x + 1 unless x == 3; aa}, which was faster than building the array, but still considerably slower than the three 3 fast ways.Friarbird
This would work, but you'd have to worry about cases where the original array had legitimate nils in them.Prosody
@Andrew G. That's true, though in my particular case that would never happen. Good side-effect worth noting though.Convince
I dunno about using map{}.compact!, b/c it returns nil if nothing was skipped. I guess if you can be guaranteed that at least one element will be skipped...Lambda
You should consider performance before using this. I think it's better just use each instead of these fancy magicJanettjanetta
I like this solution, but the next is misleading because it's not really skipping the collect, it's just stopping the execution of the block and adding nil to the array. You could write it more clearly and cleanly like this: (1..4).collect { |x| x + 1 unless x == 3 }.compact or even better and more clear in my mind: (1..4).select {|n| n != 3}.map {|x| x + 1}Euhemerize
C
11

In Ruby 2.7+, it’s possible to use filter_map for this exact purpose. From the docs:

Returns an array containing truthy elements returned by the block.

(0..9).filter_map {|i| i * 2 if i.even? }   #=> [0, 4, 8, 12, 16]
{foo: 0, bar: 1, baz: 2}.filter_map {|key, value| key if value.even? }  #=> [:foo, :baz]

For the example in the question: (1..4).filter_map { |x| x + 1 unless x == 3 }.

See this post for comparison with alternative methods, including benchmarks.

Coble answered 2/8, 2019 at 9:35 Comment(1)
Great find :) It also works with next as tried by the OP: numbers.filter_map { |i| next if i.odd?; i * 2 }Bile
S
5

just a suggestion, why don't you do it this way:

result = []
(1..4).each do |x|
  next if x == 3
  result << x
end
result # => [1, 2, 4]

in that way you saved another iteration to remove nil elements from the array. hope it helps =)

Stace answered 1/3, 2011 at 8:58 Comment(8)
This is like a much more verbose version of Mladen's reject example.Maura
actually nope, "reject" iterates through the array once, and "collect" iterates through the array once again, so there's two iterations. in the case of "each" it does so only once =)Stace
This is a reasonable idea, but in Ruby it's often very interesting to do the speed test and see what actually happens. In my quick benchmark (which might or might not match Andrew's real case, of course), building the array this way is actually about twice as slow as any of the other ways. I suspect the issue is that iterating through the array in C is actually a lot faster than appending items at the Ruby << level.Friarbird
This is using mutable state, rather than a functional programming style.Prosody
There's actually no need for next here. Simply result << x unless x == 3 would work and is a bit cleaner.Convince
@Stace I apologize, you are correct. That said, I still think the prettier and faster solutions win this round.Maura
@benson: no worries benson =) @andrew: yeah u r right, 1 vote up =)Stace
@Stace Ideally the interpreter would optimize it down to a single loop.Fardel
B
0

i would suggest to use:

(1..4).to_a.delete_if {|x| x == 3}

instead of the collect + next statement.

Barge answered 1/3, 2011 at 8:48 Comment(1)
The real code is more complicated than the abstracted code in my question, so this wouldn't achieve what I'm looking for, unfortunately. (The collect actually manipulates the value, rather than just returning it)Convince
E
0

You could pull the decision-making into a helper method, and use it via Enumerable#reduce:

def potentially_keep(list, i)
  if i === 3
    list
  else
    list.push i
  end
end
# => :potentially_keep

(1..4).reduce([]) { |memo, i| potentially_keep(memo, i) }
# => [1, 2, 4]
Edita answered 15/6, 2014 at 5:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.