How to do pythonic list comprehensions with conditions in ruby?

Python has an elegant syntax for taking all or part of a list and optionally transforming its values, should they meet zero or more conditions. It's called "list comprehensions", and looks like this:

In [1]: [x for x in range(10) if x % 2 == 0]
Out[1]: [0, 2, 4, 6, 8]

(...er, that's without the transformation part. But that's irrelevant for this question...)

I'm not finding a "simple" or "elegant" way of doing this in ruby, but maybe I just don't know where to look. The best I've found is this:

irb(main):031:0> (0..10).collect {|x| x unless x % 2 == 0} .compact
=> [1, 3, 5, 7, 9]

...which, shall we say, is inelegantly adequate. You have to add the .compact part to remove the

nil

s that will be left in otherwise:

irb(main):032:0> (0..10).collect {|c| c unless c%2==0}
=> [nil, 1, nil, 3, nil, 5, nil, 7, nil, 9, nil]

What's the better way? I'm hoping there is one...

Trackback URL for this post:

http://onebiglibrary.net/trackback/80

jaf (not verified) on June 23rd 2006

Ok, so I'm poking around to see if I can answer your call, and learn something myself.

Might this do the trick:

a.delete_if {|x| x % 2 == 0}

I realize this only handles deletes, but I think that's the only special case where you're running into the nil issue. Otherwise, the collect! method should work for other types of transformations...

Bruce D'Arcus (not verified) on June 24th 2006

Am hardly an expert, but these both achieve the same without the additional method:

(0..10).find_all {|x| x if x % 2 == 0}

(0..10).select {|x| x if x % 2 == 0}

Bruce D'Arcus (not verified) on June 24th 2006

you don't just want to select the values of course.

Ed Summers (not verified) on June 24th 2006

I'd probably do something like this:

(0..10).find_all {|x| x % 2 == 0}

Honestly, I consider list comprehensions to be one of the least elegant aspects of python--which says a lot about the overall elegance of the language I guess.

Ed Summers (not verified) on June 24th 2006

Ok, so I meant:

(0..10).find_all {|x| x % 2 != 0}

dchud on June 26th 2006

Thanks, all for the suggestions. I should have been more precise: I want to be able to do arbitrary tranformations in-line, too. More like the following python:

In [1]: [x*3 for x in range(10) if x%2 == 0]
Out[1]: [0, 6, 12, 18, 24]

Trying all of your suggestions above, I'm stuck with:

irb(main):015:0* (0..10).find_all {|x| x*3 if x%2 == 0}
=> [0, 2, 4, 6, 8, 10]
irb(main):016:0> (0..10).select {|x| x*3 if x%2 == 0}
=> [0, 2, 4, 6, 8, 10]
irb(main):017:0> (0..10).collect {|x| x*3 if x%2 == 0}
=> [0, nil, 6, nil, 12, nil, 18, nil, 24, nil, 30]
irb(main):018:0> (0..10).collect {|x| x*3 if x%2 == 0}.compact
=> [0, 6, 12, 18, 24, 30]

That's frustrating. Why aren't the values transforming with select and find_all? Is something wrong above? This is ruby-1.8.4, locally built on centos4-x86_64, fwiw.

dchud on June 26th 2006

This works for lists, whereas above all the examples are ranges. Interesting.

irb(main):043:0> d = [0,1,2,3,4,5,6,7,8,9,10]
=> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
irb(main):044:0> d.select {|x| x*3 if x%2==0}
=> [0, 2, 4, 6, 8, 10]
irb(main):045:0> d.find_all {|x| x*3 if x%2==0}
=> [0, 2, 4, 6, 8, 10]

Hmm. Why is a range not like a list? /me scratches head and scores one for python on being more intuitive here.

David Conrad (not verified) on August 11th 2006

I think what you want is something like:

(1..10).find_all {|x| x%2 == 0}.map {|x| x*3}

David Conrad (not verified) on August 11th 2006

If you really want to find and map with one block you could roll it up yourself like this:

class Array
  def find_map
    return self unless block_given?
    result = []
    self.each do |i|
      r = yield i
      result << r unless r.nil?
    end
    result
  end
end

You have to add it to Range separately, but we can just pass the buck to the new Array method:

class Range
  def find_map
    return self.collect unless block_given?
    self.collect.find_map {|x| yield x}
  end
end

Now you can use find_map with an Array or a Range like so:

>> (1..10).find_map {|x| x*3 if x%2 == 0}
=> [6, 12, 18, 24, 30]
>> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].find_map {|x| x*3 if x%2 == 0}
=> [6, 12, 18, 24, 30]

P.S. How do you do this insanely great code format preserving and syntax highlighting? It's fantastic!

Jim (not verified) on March 23rd 2007

Not sure what a range is in Python, but I'd say it is wrong. (Read on for why)

python: [x for x in range(10) if x % 2 == 0]
Out[1]: [0, 2, 4, 6, 8]

To get this same result for Ruby, it takes one more character
(notice the triple '...')

ruby: (0...10).select {|x| x if x % 2 == 0}
[0, 2, 4, 6, 8]

I would expect range to include both upper and lower bounds:
ruby: (0..10).select {|x| x if x % 2 == 0}
[0, 2, 4, 6, 8, 10]

The trouble with list comprehensions, IMO, is that they are special.
And what a silly name (IMO). Block are pervasive in Ruby.
Nothing special here. No extra brain power at all.

A simple 'ri Array' would have found the select method.
----------------------------------------------------------- Array#select
array.select {|item| block } -> an_array
------------------------------------------------------------------------
Invokes the block passing in successive elements from _array_,
returning an array containing those elements for which the block
returns a true value (equivalent to +Enumerable#select+).

a = %w{ a b c d e f }
a.select {|v| v =~ /[aeiou]/} #=> ["a", "e"]

Notice how you can expect Ruby to do the right thing and the iterator
treats the Range similar to an array.

raggi (not verified) on May 28th 2007

The block passed to select needs to return a boolean, not a value.

>> (0..10).select {|x| x % 2 == 0}
=> [0, 2, 4, 6, 8, 10]

There was no need for x if x % 2 ==0, only the boolean. Amusing choice of title, though.

I see the selects being written here attempting to operate on the variable. If this does do anything, it will be in-place on the array (or range) that you're operating on. For an array, this is not really a problem, other than you probably didn't want to do that:

>> a = ('a'..'z').to_a
=> ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]
>> a.select do |l| l =~ /[aeiou]/ ? (l &lt;&lt; '-mod'; l) : nil end 
=> ["a-mod", "e-mod", "i-mod", "o-mod", "u-mod"]
>> a
=> ["a-mod", "b", "c", "d", "e-mod", "f", "g", "h", "i-mod", "j", "k", "l", "m", "n", "o-mod", "p", "q", "r", "s", "t", "u-mod", "v", "w", "x", "y", "z"]

Obviously, you could make a copy of the array at this point, and it would solve the problem, at the obvious cost. See at the end for some examples of another way to achieve a similar effect in 'one line'.

With a range, the outcome is different:

>> a = ('a'..'z')
=> "a".."z"
>> a.select do |l| l =~ /[aeiou]/ ? (l &lt;&lt; '-mod'; l) : nil end
=> ["a-mod"]
>> a
=> "a-mod".."z"

Why? because you've in-place modified the range. Now, what is an enumerator to do with a range, when you've modified it in-line? What is the next item from 'a-mod'..'z' from 'a'?
'a' is no longer in the range.

If you want numeric iteration over a list, you first of all need a list!

>> r=[]; ('a'..'z').each {|v| r &lt;&lt; v if v =~ /[aeiou]/}; r
=> ["a", "e", "i", "o", "u"]
>> y = []; for x in 0..10 do y &lt;&lt; x * 3 if x % 2 == 0 end; y
=> [0, 6, 12, 18, 24, 30]

The last ';var' is only for irb to print the array we just created, you can remove it, or, if you really really want to, you could do this (might be suitable for a function)...
var = (y = []; for x in 0..10 do y &lt;&lt; x * 3 if x % 2 == 0 end; y)

There are some things in the ruby grammar I truly love, no matter how 'useless' they may seem.

raggi (not verified) on May 28th 2007

>> (0..10).to_a.inject([]) do |a,x| a &lt;&lt; x * 3 if x % 2 == 0; a end  
=> [0, 6, 12, 18, 24, 30]  

tokland (not verified) on January 01st 2008

I have just learnt Ruby today so I may be wrong, but I would first filter with 'select' and then apply the function with 'collect':

(0..10).select {|x| (x%2) == 0 }.collect {|x| 3*x}
=> [0, 6, 12, 18, 24, 30]

Is this idiomatic Ruby?

Post new comment

The content of this field is kept private and will not be shown publicly.
  • Allowed HTML tags: <a> <em> <strong> <cite> <pre> <code> <img> <ul> <ol> <li> <dl> <dt> <dd> <blockquote> <form> <input> <span> <object> <embed> <br>
  • Lines and paragraphs break automatically.
  • You can enable syntax highlighting of source code with the following tags: <code>, <blockcode>, <apache>, <bash>, <css>, <diff>, <dot>, <java>, <javascript>, <mysql>, <perl>, <php>, <python>, <rails>, <ruby>, <sql>, <xml>. Beside the tag style "<foo>" it is also possible to use "[foo]".

More information about formatting options

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.
8 + 1 =
Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.