toolmantim

instance_eval brings sexy back

November 30, 2006 10:04 (Sydney Australia)

Accessing regex matches can often create ugly code.

For example, take the following code:

time_components = /(\d+):(\d+):(\d+)/.match("17:00:34")
time_components[1] # => "17" 
time_components[2] # => "00" 
time_components[3] # => "34" 

That’s not too bad in this trivial example, but if you start to use the regex Match object multiple times it starts to create ugly code. “[1]” isn’t exactly the most descriptive label for that chunk of data is it?

Marcel’s funky post on projectionist showing a sexy use of just-in-time methods using instance_eval and class << has had me thinking lately, and I refactored some code yesterday to make these regex’s a bit more sexy.


time_components = /(\d+):(\d+):(\d+)/.match("17:00:34")
time_components.instance_eval do
  def hours; self[1] end
  def minutes; self[2] end
  def seconds; self[3] end
end
You can now just refer to the components by what they actually represent:

time_components.hours # => "17" 
time_components.minutes # => "00" 
time_components.seconds # => "34" 
time_components.class # => MatchData

How sexy is that? And look mah, it’s still a MatchData object.

You can make it even sexier and break it down to a single statement by making instance_eval return self:


time_components = /(\d+):(\d+):(\d+)/.match("17:00:34").instance_eval do
  def hours; self[1] end
  def minutes; self[2] end
  def seconds; self[3] end
  self
end

time_components.hours # => "17" 

In the app I’m working on we’re calling out to the system to get some stats, and then munging that data to provide information to the view. Combining the above techniques I ended up with:


def memory
  @memory ||= begin
    system_sysctl.instance_eval do
      def values; split.map(&:to_i) end
      def total; values.inject(0) { |v, total| total + v } end
      def free; values.last end
      def used; total - free end
      def percent_used; (used.to_f / total.to_f) * 100 end
      self
    end
  end
end

def system_sysctl
  `sysctl ...`
end
And now my view can simply refer to the following:
@system.memory.percent_used

Hot, no?

Comments

James Hill

mmm.. instance_eval is sexy i just used it a while back for a nearly identical use.. adding a yearweek method similar to sql’s yearweak to a date object.


date.instance_eval do
   def yearweek
         self.cwyear.to_s + sprintf(“%02d”, self.cweek)
   end
end

James Hill

if only it was formatted!!!

Tim Lucas

If only ;)

James Hill

thanks mate : D

Myles Byrne

Nice work tim. Doing an index access on matchdata objects has always bugged me too.

You could also give the matchdata class some love directly:


class MatchData
  def matchnames(*names)
    names.each_with_index do |name, index|
      self.instance_eval "def #{name}; self[#{index+1}] end" 
    end
    self
  end
end

and then do:


time_components = /(\d+):(\d+):(\d+)/.match("17:00:34").matchnames(:hours, :mins, :secs)
time_components.hours

Tim Lucas

Now that, is hot.

How about adding matchnames to NilClass and have it return nil, so you can still keep the sexy chaining if it doesn’t match the string.

Chris

hot hot hot.

(Tim + Myles) += 1

Peter Cooper

I’ve posted more about this, along with an implementation for String#match (my preferred poison) at http://www.rubyinside.com/improving-stringmatch-with-instance_eval-315.html

Thanks guys!

Johan Tam

syntax error, unexpected ’;’ def values; split.map(&;:to_i) end

An error came out while running your code above, what exactly does “&;:to_i’ mean then?

Tim Lucas

Whoops. Johan: problem with RedCloth! Sorry…

To comment on this article you must have javascript enabled.