I’m currently busy learning Ruby and Rails, and since I have a background in C-based languages, some concepts of Ruby are new and somewhat alien. Especially challenging for me is adapting to the “Ruby-way” of approaching common problems, hence I often find myself coding C in Ruby, which is not what I’m trying to achieve.
Imagine having a schema like this:
ActiveRecord::Schema.define(:version => 20111119180638) do
create_table "bikes", :force => true do |t|
t.string "Brand"
t.string "model"
t.text "description"
end
end
The database already contains several different bikes. My goal is to get an array of all brands represented in the database.
Here is my code:
class Bike < ActiveRecord::Base
def Bike.collect_brands
temp_brands = Bike.find_by_sql("select distinct brand from bikes")
brands = Array.new
temp_brands.each do |item|
brands.push(item.brand)
end
brands
end
end
How would a Ruby guru write code to achieve this?
tl;dr: Your entire method can be replaced with
Bike.uniq.pluck(:brand).This functionality already exists (see the end of my answer), but first, lets step through your code and make it more idiomatic:
First and foremost, use two spaces per level of indentation, not four, not eight, and not tabs. Use two spaces. This is not personal preferences, this is an extremely strong convention within the Ruby community and pretty much required if you intend to participate.
Next, there almost never a good reason to use this pattern in Ruby:
When you want to translate one array into another array (really, one Enumerable into another Enumerable) by applying some code to each of values in the input array, use
maporcollect(which are synonyms):Next, you can take advantage of
symbol#to_procto make the above code a little clearer:This will look strange to the uninitiated, but it is clearer once you’re used to working with
mapand&:field. A little bit of experience will make the intent of this line of code very obvious: It’s applying thebrandmethod to each element in the array, and it’s exactly equivalent to the previous{ |item| item.brand }version.Now, your entire method can become a pretty simple one-liner:
That inline select/distinct SQL is kind of ugly, especially since ActiveRecord already lets us select specific fields with
select, and make the results distinct usinguniq:As a final iteration we can use
pluckinstead ofmapto pull only the fields out of the results that we’re interested in. But, becausepluckactually modifies the SQL being generated to only include the fields being plucked, we can omit theselect(:brand)portion, and our code boils down to an incredibly short single line containing two chained methods:Note that the order is important because
pluckalways returns an array, not an ActiveRecord relation ready for additional method chaining.Bike.pluck(:brand).uniqwould select the brand from every record (select brand from bikes) and then, in Ruby, reduce the array to the unique items. Potentially a very expensive operation.And that’s it,
Bike.uniq.pluck(:brand). As a C programmer, you’ll find that many of repetitive tasks you’re used to doing with small loops are practically already solved for you by the language itself or by supporting libraries. The amount of code you don’t write can be very surprising, once you’ve learned to write idiomatic Ruby and Rails code.