Topic: 5 Ruby/Rails Tips

- Tip 1

Do you frequently find yourself coding up models such as Person or Company that have a attributes like city, state, and country? This kind of data is usually represented in one way for most purposes. This tip will give an example implementation and describe how to make it reusable.

def location
  locs = []
  locs << city unless city.blank?
  locs << state unless state.blank?
  locs << country unless country.blank?

  loc_str = locs.join ', '
  if loc_str.empty?
    return "unspecified"
  else
    return loc_str
  end
end


Here, the string returned is of the form <city, state, country> if city, state, and country are all filled in, or <city, country> if state is not filled in, or <city, state> if country is nil or empty or just <city> if neither city nor country are filled in. I'm sure you get the idea. If no location data is filled in, the return value defaults to "unspecified."

Side note: By the way, why not just make one field called location in your model to avoid all this hassle? Well you could, but then you make it much harder to add certain functionality in the future. For example, say you wanted a list of users from the same city. This task is more difficult if you have to first parse an arbitrarily-formatted location string and try to find the city, state, and country in it. Sometime it's best to burn as few bridges as possible when designing the data model for an evolving application.

We are going to make this method reusable by putting it into a module accessible as a mixin. Ruby mixins facilitate code reuse without complicating the inheritance chain like an abstract class in something like Java would. They are also a flexible way to handle the multiple inheritance problem.

First, create a new file in the lib subdirectory of your Rails root directory:

lib/locatable.rb:

module Locatable
  def location
    # [implementation code from above goes here]
  end
end

Rails automatically loads files in the lib directory, so there's no need to require 'locatable.rb' anywhere.

Then, to call the mixin from a model like Person, do the following:

app/models/Person.rb:

def Person < ActiveRecord::Base
  # assume the people table has fields city, state, and country, which may or may not be filled
  include Locatable
end

- Tip 2

The following helper function can be used to compare to arbitrary objects (possible of different types) as long as they both have a particular attribute.

def compare_by(obj_1, obj_2, attribute)
  attr_1 = obj_1.send attribute
  attr_2 = obj_2.send attribute
  return attr_1 <=> attr_2      # note: <=> is the comparator method defined for attr_1 objects
end

There are a few scenarios where such a function is useful. An interesting example is for custom sorting:

people = Person.find(:all)
sorted = people.sort do |p1, p2|
  compare_by(p1, p2, :name)
end

The above code will sort an array of people by name. Of course :name can be replaced with an integral field like :age or even a more complex field like :location, provided that a comparator function is defined for it.

Of course you can also do something simpler with the compare_by function:

p1 = Person.find(1)
p2 = Person.find(2)
cmp = compare_by(p1, p2, :name)
if cmp > 0
  # p1 comes after p2
elsif cmp < 0
  # p1 comes before p2
else
  # p1 and p2 are "equal"
end

- Tip 3

Now let's talk about how to define a comparator for your models. Comparators in Ruby are similar to their Java counterparts, with a few notable differences. Here's how you define one, once again using our trusty Person model:

class Person < ActiveRecord::Base
  def <=>(rhs)
    if age < rhs.age
      # a younger person should be < an older person
      return -1
    elsif age > rhs.age
      # an older person should be > a younger person
      return 1
    else
      # two people of the same age should be ==
      return 0
    end
  end
end

So as you can see, it's not much different from any other method. You can still call it like you'd expect:

john = Person.find(1)
jane = Person.find(2)
john.<=>(jane)

But Ruby has a bit of syntactical sugar that allows you to write this with the same effect:

john <=> jane

At this point it's useful to note that the comparator above can be rewritten much more concisely:

def <=> rhs
  return age <=> rhs.age
end

Here, our comparator for Person is defined using Fixnum's, since we're comparing people by age and age is a Fixnum.

We can build arbitrarily complex comparison hierarchies by defining the comparator to suite your priorities. For example, say you want to compare cars. Clearly a Porsche is better than a Hyundai, but what about comparing two Porsches? We can use the make and model attributes of a car class in our comparator:

class Car
  def <=> rhs
    by_make = make <=> rhs.make
    if by_make == 0               # if the two cars are of the same make
      return model <=> rhs.model  # compare them by model
    else
      return by_make              # otherwise just base the comparison on make
    end
  end
end

Assuming meaningful comparators are defined for make and model objects, the above comparator should tell us that porsche_boxster > hyundai_sonata and porsche_boxster < porsche_911.


- Tip 4

Suppose you have a User model which has a couple boolean attributes which, following convention, are accessible with methods ending in a question mark: logged_in? to check if the given user is logged in and moderator? to check if the user is a moderator or a regular user. Now suppose you'd like a list of moderators and a list of regular users that are currently logged in. Those of you coming from Java or some other C-style language would probably loop over the list of users and, at each iteration, decide wether to put the current user into the logged in moderators list, the logged in users list, or neither. This is of course a valid approach, but now let's take a look at a higher-level approach that is more idiomatic Ruby:

logged_in = User.find(:all).select do |user|
  user.logged_in?
end

logged_in_mods, logged_in_users = logged_in.partition do |user|
  user.moderator?
end


The above method uses a couple of handy standard Ruby methods: select and partition. There is a more compact way to write the same code by using Symbol#to_proc.

logged_in_mods, logged_in_users = User.find(:all).select(&:logged_in?).partition(&:moderator?)

The latter is clearly more compact, but I leave the reader to compare the readability of the two snippets.


- Tip 5

I'll wrap up this series with a tip about the always-useful IRB console. I recommend opening script/console and following along for a bit since these examples are much easier to understand if you see them in action. This way, if you're feeling adventurous, you can try and tweak them a bit to come up with something interesting.

Imagine that you forgot some obscure method name, e.g. has_and_belongs_to_many. You remember that it contains the substring has, but you're not sure how to formulate a Google query to answer your question. An easy solution is to start up script/console and type the following:

ActiveRecord::Base.methods.grep /^has/

ActiveRecord::Base is the class whose methods we want to search. You know that the association methods (including has_and_belongs_to_many) are part of a Rails model which inherits from the Base class of the ActiveRecord module. methods() is a method that all Ruby classes have that returns an array of all the methods in that class (their names as strings). Then array.grep searches array for its parameter, in this case /^has/, which is a regular expression as denoted by the slashes instead of quotes (^ is the start of the string).

Another example is if you wanted a list of filter methods available to a controller:

ActionController::Base.methods.grep /filter/

Or if you wanted a list of methods in your User model that followed the naming conventions signifying that they returned booleans:

User.methods.grep /\?$/

Here ? is a special character so it has the be escaped with a \ to signify that you mean a literal ? instead of its special meaning (zero or one occurrences of the previous pattern) and $ indicates the end of the string.

grep is just one example of a method that takes a regular expression as a parameter. For those of you unfamiliar with regular expressions, I recommend reading up on them if you're interested in a powerful tool to search and match strings that's available in most mainstream languages.

Last edited by iamvlad (2008-05-06 04:05:45)

Re: 5 Ruby/Rails Tips

iamvlad wrote:

- Tip 1

def location
  locs = []
  locs << city unless (city.nil? || city.empty?)
  locs << state unless (state.nil? || state.empty?)
  locs << country.name unless (country.nil? || country.name.empty?)

  loc_str = locs.join ', '
  if loc_str.empty?
    return "unspecified"
  else
    return loc_str
  end
end

Thanks for the tips, I'm wondering if you could just use city.blank? rather than (city.nil? || city.empty?)

  locs << city unless city.blank?

WorkingWithRails.com
Person.recommend(jason_deppen)

Re: 5 Ruby/Rails Tips

jdeppen wrote:
iamvlad wrote:

- Tip 1

def location
  locs = []
  locs << city unless (city.nil? || city.empty?)
  locs << state unless (state.nil? || state.empty?)
  locs << country.name unless (country.nil? || country.name.empty?)

  loc_str = locs.join ', '
  if loc_str.empty?
    return "unspecified"
  else
    return loc_str
  end
end

Thanks for the tips, I'm wondering if you could just use city.blank? rather than (city.nil? || city.empty?)

  locs << city unless city.blank?

Yes - you could.

Re: 5 Ruby/Rails Tips

Good call guys, I actually didn't know about blank? before. I've updated the tip to reflect your suggestion.

Apparently blank? is a method that Rails adds because it doesn't seem to work in vanilla Ruby (though I guess you could easily add it to String and NilClass).