List comprehension in Ruby

RubyList Comprehension

Ruby Problem Overview


To do the equivalent of Python list comprehensions, I'm doing the following:

some_array.select{|x| x % 2 == 0 }.collect{|x| x * 3}

Is there a better way to do this...perhaps with one method call?

Ruby Solutions


Solution 1 - Ruby

How 'bout:

some_array.map {|x| x % 2 == 0 ? x * 3 : nil}.compact

Slightly cleaner, at least to my taste, and according to a quick benchmark test about 15% faster than your version...

Solution 2 - Ruby

If you really want to, you can create an Array#comprehend method like this:

class Array
  def comprehend(&block)
    return self if block.nil?
    self.collect(&block).compact
  end
end

some_array = [1, 2, 3, 4, 5, 6]
new_array = some_array.comprehend {|x| x * 3 if x % 2 == 0}
puts new_array

Prints:

6
12
18

I would probably just do it the way you did though.

Solution 3 - Ruby

I made a quick benchmark comparing the three alternatives and map-compact really seems to be the best option.

Performance test (Rails)
require 'test_helper'
require 'performance_test_help'

class ListComprehensionTest < ActionController::PerformanceTest
  
  TEST_ARRAY = (1..100).to_a

  def test_map_compact
    1000.times do
      TEST_ARRAY.map{|x| x % 2 == 0 ? x * 3 : nil}.compact
    end
  end

  def test_select_map
    1000.times do
      TEST_ARRAY.select{|x| x % 2 == 0 }.map{|x| x * 3}
    end
  end

  def test_inject
    1000.times do
      TEST_ARRAY.inject([]) {|all, x| all << x*3 if x % 2 == 0; all }
    end
  end

end
Results
/usr/bin/ruby1.8 -I"lib:test" "/usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake/rake_test_loader.rb" "test/performance/list_comprehension_test.rb" -- --benchmark
Loaded suite /usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake/rake_test_loader
Started
ListComprehensionTest#test_inject (1230 ms warmup)
           wall_time: 1221 ms
              memory: 0.00 KB
             objects: 0
             gc_runs: 0
             gc_time: 0 ms
.ListComprehensionTest#test_map_compact (860 ms warmup)
           wall_time: 855 ms
              memory: 0.00 KB
             objects: 0
             gc_runs: 0
             gc_time: 0 ms
.ListComprehensionTest#test_select_map (961 ms warmup)
           wall_time: 955 ms
              memory: 0.00 KB
             objects: 0
             gc_runs: 0
             gc_time: 0 ms
.
Finished in 66.683039 seconds.

15 tests, 0 assertions, 0 failures, 0 errors

Solution 4 - Ruby

There seems to be some confusion amongst Ruby programmers in this thread concerning what list comprehension is. Every single response assumes some preexisting array to transform. But list comprehension's power lies in an array created on the fly with the following syntax:

squares = [x**2 for x in range(10)]

The following would be an analog in Ruby (the only adequate answer in this thread, AFAIC):

a = Array.new(4).map{rand(2**49..2**50)} 

In the above case, I'm creating an array of random integers, but the block could contain anything. But this would be a Ruby list comprehension.

Solution 5 - Ruby

I discussed this topic with Rein Henrichs, who tells me that the best performing solution is

map { ... }.compact

This makes good sense because it avoids building intermediate Arrays as with the immutable usage of Enumerable#inject, and it avoids growing the Array, which causes allocation. It's as general as any of the others unless your collection can contain nil elements.

I haven't compared this with

select {...}.map{...}

It's possible that Ruby's C implementation of Enumerable#select is also very good.

Solution 6 - Ruby

An alternative solution that will work in every implementation and run in O(n) instead of O(2n) time is:

some_array.inject([]){|res,x| x % 2 == 0 ? res << 3*x : res}

Solution 7 - Ruby

I've just published the comprehend gem to RubyGems, which lets you do this:

require 'comprehend'

some_array.comprehend{ |x| x * 3 if x % 2 == 0 }

It's written in C; the array is only traversed once.

Solution 8 - Ruby

Enumerable has a grep method whose first argument can be a predicate proc, and whose optional second argument is a mapping function; so the following works:

some_array.grep(proc {|x| x % 2 == 0}) {|x| x*3}

This isn't as readable as a couple of other suggestions (I like anoiaque's simple select.map or histocrat's comprehend gem), but its strengths are that it's already part of the standard library, and is single-pass and doesn't involve creating temporary intermediate arrays, and doesn't require an out-of-bounds value like nil used in the compact-using suggestions.

Solution 9 - Ruby

[1, 2, 3, 4, 5, 6].collect{|x| x * 3 if x % 2 == 0}.compact
=> [6, 12, 18]

That works for me. It is also clean. Yes, it's the same as map, but I think collect makes the code more understandable.


select(&:even?).map()

actually looks better, after seeing it below.

Solution 10 - Ruby

This is more concise:

[1,2,3,4,5,6].select(&:even?).map{|x| x*3}

Solution 11 - Ruby

Like Pedro mentioned, you can fuse together the chained calls to Enumerable#select and Enumerable#map, avoiding a traversal over the selected elements. This is true because Enumerable#select is a specialization of fold or inject. I posted a hasty introduction to the topic at the Ruby subreddit.

Manually fusing Array transformations can be tedious, so maybe someone could play with Robert Gamble's comprehend implementation to make this select/map pattern prettier.

Solution 12 - Ruby

Something like this:

def lazy(collection, &blk)
   collection.map{|x| blk.call(x)}.compact
end

Call it:

lazy (1..6){|x| x * 3 if x.even?}

Which returns:

=> [6, 12, 18]

Solution 13 - Ruby

Ruby 2.7 introduced filter_map which pretty much achieves what you want (map + compact):

some_array.filter_map { |x| x * 3 if x % 2 == 0 }

You can read more about it here.

Solution 14 - Ruby

Another solution but perhaps not the best one

some_array.flat_map {|x| x % 2 == 0 ? [x * 3] : [] }

or

some_array.each_with_object([]) {|x, list| x % 2 == 0 ? list.push(x * 3) : nil }

Solution 15 - Ruby

This is one way to approach this:

c = -> x do $*.clear             
  if x['if'] && x[0] != 'f' .  
    y = x[0...x.index('for')]    
    x = x[x.index('for')..-1]
    (x.insert(x.index(x.split[3]) + x.split[3].length, " do $* << #{y}")
    x.insert(x.length, "end; $*")
    eval(x)
    $*)
  elsif x['if'] && x[0] == 'f'
    (x.insert(x.index(x.split[3]) + x.split[3].length, " do $* << x")
    x.insert(x.length, "end; $*")
    eval(x)
    $*)
  elsif !x['if'] && x[0] != 'f'
    y = x[0...x.index('for')]
    x = x[x.index('for')..-1]
    (x.insert(x.index(x.split[3]) + x.split[3].length, " do $* << #{y}")
    x.insert(x.length, "end; $*")
    eval(x)
    $*)
  else
    eval(x.split[3]).to_a
  end
end 

so basically we are converting a string to proper ruby syntax for loop then we can use python syntax in a string to do:

c['for x in 1..10']
c['for x in 1..10 if x.even?']
c['x**2 for x in 1..10 if x.even?']
c['x**2 for x in 1..10']

# [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# [2, 4, 6, 8, 10]
# [4, 16, 36, 64, 100]
# [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

or if you don't like the way the string looks or having to use a lambda we could forego the attempt to mirror python syntax and do something like this:

S = [for x in 0...9 do $* << x*2 if x.even? end, $*][1]
# [0, 4, 8, 12, 16]

Solution 16 - Ruby

https://rubygems.org/gems/ruby_list_comprehension

shameless plug for my Ruby List Comprehension gem to allow idiomatic Ruby list comprehensions

$l[for x in 1..10 do x + 2 end] #=> [3, 4, 5 ...]

Solution 17 - Ruby

I think the most list comprehension-esque would be the following:

some_array.select{ |x| x * 3 if x % 2 == 0 }

Since Ruby allows us to place the conditional after the expression, we get syntax similar to the Python version of the list comprehension. Also, since the select method does not include anything that equates to false, all nil values are removed from the resultant list and no call to compact is necessary as would be the case if we had used map or collect instead.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionreadonlyView Question on Stackoverflow
Solution 1 - Rubyglenn mcdonaldView Answer on Stackoverflow
Solution 2 - RubyRobert GambleView Answer on Stackoverflow
Solution 3 - RubyknutonView Answer on Stackoverflow
Solution 4 - RubyMarkView Answer on Stackoverflow
Solution 5 - RubyjvoorhisView Answer on Stackoverflow
Solution 6 - RubyPedro RoloView Answer on Stackoverflow
Solution 7 - RubyhistocratView Answer on Stackoverflow
Solution 8 - RubyPeter MoulderView Answer on Stackoverflow
Solution 9 - RubyVinceView Answer on Stackoverflow
Solution 10 - RubyanoiaqueView Answer on Stackoverflow
Solution 11 - RubyjvoorhisView Answer on Stackoverflow
Solution 12 - RubyAlexandre MagroView Answer on Stackoverflow
Solution 13 - RubyMatheus RichardView Answer on Stackoverflow
Solution 14 - RubyjoegiraltView Answer on Stackoverflow
Solution 15 - RubySam MichaelView Answer on Stackoverflow
Solution 16 - RubySam MichaelView Answer on Stackoverflow
Solution 17 - RubyChristopher RoachView Answer on Stackoverflow