110

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?

3
  • 4
    Both yours and glenn mcdonald's answers seem fine to me... I don't see what you'd gain by trying to be more concise than either. Commented Nov 23, 2008 at 16:22
  • 1
    this solution transverses the list two times. The inject doesn't. Commented May 3, 2010 at 22:16
  • 2
    Some awesome answers here but it'd be awesome too see ideas for list comprehension across multiple collections. Commented Apr 22, 2012 at 15:01

17 Answers 17

104

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...

Sign up to request clarification or add additional context in comments.

2 Comments

as well as some_array.map{|x| x * 3 unless x % 2}.compact, which is arguably more readable/ruby-esque.
@nightpool unless x%2 has no effect since 0 is truthy in ruby. See: gist.github.com/jfarmer/2647362
59

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.

4 Comments

You could use compact! to optimize a bit
This isn't actually correct, consider: [nil, nil, nil].comprehend {|x| x } which returns [].
alexey, according to the docs, compact! returns nil instead of the array when no items are changed, so I don't think that works.
For what it's worth, this is not a new syntactical construct, so it's hardly a "comprehension". I personally don't think it's appropriate to name such a function "comprehend"
35

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 

2 Comments

Would be interesting to see reduce in this benchmark as well (see stackoverflow.com/a/17703276).
map_compact maybe faster but it's creating a new array. inject is space efficient than map.compact and select.map
14

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.

7 Comments

How would you do what the OP is trying to do?
Actually I see now the OP itself had some existing list the author wanted to transform. But the archetypal conception of list comprehension involves creating an array/list where one didn't exist before by referencing some iteration. But actually, some formal definitions say list comprehension can't use map at all, so even my version isn't kosher -but as close as one could get in Ruby I guess.
I don't get how your Ruby example is supposed to be an analogue of your Python example. The Ruby code should read: squares = (0..9).map { |x| x**2 }
While @michau is right, the whole point of list comprehension (which Mark neglected) is that list comprehension itself does not use not generate arrays - it uses generators and co routines to do all calculations in a streaming manner without allocating storage at all (except temp variables) until (iff) the results land in an array variable - this is the purpose of the square brackets in the python example, to collapse the comprehension to a set of results. Ruby has no facility similar to generators.
Oh yes, it has (since Ruby 2.0): squares_of_all_natural_numbers = (0..Float::INFINITY).lazy.map{|x| x**2}; p squares_of_all_natural_numbers.take(10).to_a
|
12

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.

Comments

10

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.

1 Comment

Sadly, naming something comprehend doesn't make it a comprehension.
9

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} 

3 Comments

You mean it traverses the list only once. If you go by the formal definition, O(n) equals O(2n). Just nitpicking :)
@Daniel Harper :) Not only you are right, but also for the average case, transversing the list once to discard some entries, and then again to perform an operation can be acctually better in the average cases :)
In other words, you are doing 2 things n times instead of 1 thing n times and then another 1 thing n times :) One important advantage of inject/reduce is that it preserves any nil values in the input sequence which is more list-comprehensionly behaviour
7

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.

Comments

6

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.

Comments

5

This is more concise:

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

1 Comment

Or, for even more point-free awesomeness [1,2,3,4,5,6].select(&:even?).map(&3.method(:*))
4
[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.

Comments

3

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] 

1 Comment

What's wrong with defining lazy on Array and then: (1..6).lazy{|x|x*3 if x.even?}
2

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.

Comments

1

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 } 

Comments

0

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] 

Comments

0

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 ...] 

Comments

-3

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.

1 Comment

This doesn't appear to work. At least in Ruby 1.8.6, [1,2,3,4,5,6].select {|x| x * 3 if x % 2 == 0} evaluates to [2, 4, 6] Enumerable#select only cares about whether the block evaluates to true or false, not what value it outputs, AFAIK.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.