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 ther
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.
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]
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.
I made a quick benchmark comparing the three alternatives and map-compact really seems to be the best option.
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
/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
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]
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.