In Ruby, is there an Array method that combines 'select' and 'map'?
I have a Ruby array containing some string values. I need to:
- Find all elements that match some predicate
- Run the matching elements through a transformation
- Return the results as an array
Right now my solution looks like this:
def example
matchingLines = @lines.select{ |line| ... }
results = matchingLines.map{ |line| ... }
return results.uniq.sort
end
Is there an Array or Enumerable method that combines select and map into a single logical statement?
Solution 1:
I usually use map
and compact
together along with my selection criteria as a postfix if
. compact
gets rid of the nils.
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]
Solution 2:
Ruby 2.7+
There is now!
Ruby 2.7 is introducing filter_map
for this exact purpose. It's idiomatic and performant, and I'd expect it to become the norm very soon.
For example:
numbers = [1, 2, 5, 8, 10, 13]
enum.filter_map { |i| i * 2 if i.even? }
# => [4, 16, 20]
Here's a good read on the subject.
Hope that's useful to someone!
Solution 3:
You can use reduce
for this, which requires only one pass:
[1,1,1,2,3,4].reduce([]) { |a, n| a.push(n*3) if n==1; a }
=> [3, 3, 3]
In other words, initialize the state to be what you want (in our case, an empty list to fill: []
), then always make sure to return this value with modifications for each element in the original list (in our case, the modified element pushed to the list).
This is the most efficient since it only loops over the list with one pass (map
+ select
or compact
requires two passes).
In your case:
def example
results = @lines.reduce([]) do |lines, line|
lines.push( ...(line) ) if ...
lines
end
return results.uniq.sort
end
Solution 4:
Another different way of approaching this is using the new (relative to this question) Enumerator::Lazy
:
def example
@lines.lazy
.select { |line| line.property == requirement }
.map { |line| transforming_method(line) }
.uniq
.sort
end
The .lazy
method returns a lazy enumerator. Calling .select
or .map
on a lazy enumerator returns another lazy enumerator. Only once you call .uniq
does it actually force the enumerator and return an array. So what effectively happens is your .select
and .map
calls are combined into one - you only iterate over @lines
once to do both .select
and .map
.
My instinct is that Adam's reduce
method will be a little faster, but I think this is far more readable.
The primary consequence of this is that no intermediate array objects are created for each subsequent method call. In a normal @lines.select.map
situation, select
returns an array which is then modified by map
, again returning an array. By comparison, the lazy evaluation only creates an array once. This is useful when your initial collection object is large. It also empowers you to work with infinite enumerators - e.g. random_number_generator.lazy.select(&:odd?).take(10)
.
Solution 5:
If you have a select
that can use the case
operator (===
), grep
is a good alternative:
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"]
If we need more complex logic we can create lambdas:
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***"]