• Random
  • Archive
  • RSS
  • Ask me anything

Ryan Angilly

A business guy who became a really good software developer first by accident

Dynamically adding class methods in Ruby

Even though there is technically no such thing as a class method in Ruby, I’m going to call them that for the sake of clarity.  When I say class method, I mean something like this:

class A
  class << self
    def yo
      "wassup"
    end
  end
end

A.yo #=> "wassup"

Every now and then, you may want to dynamically generate these things.  Thanks to instance_eval and define_method, dynamically defining methods in Ruby is trivial, but these operate in what someone coming from Java or C++ would call the instance context.  Take the following example:

class A
  class << self
    def create_method(name)
      define_method(name) { puts "Nice!  I'm #{name}" }
    end
  end
end


A.create_method('mine')
A.mine     #Raises NoMethodError
A.new.mine #prints out "Nice! I'm mine"

The define_method creates a method that is only accessible on instances of the class, not a class method. I’ve seen people struggle with this, and in some code you’ll end up with stuff like this:

class A
  class << self
    def create_method(name)
      self.class.instance_eval do
        define_method(name) { puts "Nice!  I'm #{name}" }
      end
    end
  end
end

A.create_method('mine')
A.mine     #prints out "Nice! I'm mine"

Great success! Case closed! The problem is that this is crap. It’s wrong. To understand why it’s wrong, check out the following example:

class A
  class << self
    def create_method(name)
      self.class.instance_eval do
        define_method(name) { puts "Nice!  I'm #{name}" }
      end
    end
  end
end
A.create_method('mine')
A.mine     # prints out "Nice! I'm mine"

class B
end
B.mine     # prints out "Nice! I'm mine"

What?

Calling self.class.instance_eval will evaluate the block on self.class, which in this case is Class itself, the object from which all classes descend.  Thanks to inheritance, that means that every class will get this method:

"1".class.mine    # prints out "Nice! I'm mine"
1.class.mine      # prints out "Nice! I'm mine"

The big problem with this is that it does work. It deceives you. And the real kick in the balls can come when you want to define this class method in a class, and then call it from subclasses:

class Main
  def Main.create_method(name, args)
    klass = self.to_s
    self.class.instance_eval do
      define_method(name) { return "nice! from #{klass} with #{args.inspect}" }
    end
  end
end

class A < Main
  create_method :delete, :only => 7
end
A.delete  #=> "nice! from A with {:only=>7}"

class B < Main
  create_method :delete, :only => 8
end

B.delete  #=> "nice! from B with {:only=>8}"
A.delete  #=> "nice! from B with {:only=>8}"

Yeah. The method delete doesn’t get defined separately on A and B. It gets defined on Class once, and then redefined. When A to tries and run #delete, Ruby looks in A, then Main, then Class. When B tries to run #delete, Ruby looks in B, then Main, then Class. Same method.

This technique can ruin your day.

So, Ryan, what’s the solution?

Simple. The metaclass. The metaclass is defined as:

metaclass = class << self
              self
            end

I’m not going to explain the metaclass (aka eigenclass, aka singleton class) in this post, there’s plenty of material on it. Rails ActiveSupport module gives you a method called metaclass that you can use anywhere, without ActiveSupport you’ll need to get at it yourself. But once you have it, you can do:

class Object
  def metaclass
    class << self; self; end
  end
end

class Main
  def Main.create_method(name, args)
    klass = self.to_s
    metaclass.instance_eval do
      define_method(name) { return "nice! from #{klass} with #{args.inspect}" }
    end
  end
end

class A < Main
  create_method :delete, :only => 7
end

A.delete  #=> "nice! from A with {:only=>7}"

class B < Main
  create_method :delete, :only => 8
end

B.delete  #=> "nice! from B with {:only=>8}"
A.delete  #=> "nice! from A with {:only=>7}"

You win.

Go forth and metaprogram.

    • #metaclass
    • #nerdalert
    • #ruby
    • #metaprogramming
    • #howto
  • 2 years ago
  • 1
  • Comments
  • Permalink
  • Share
    Tweet

Portrait/Logo

About

Hi, I'm Ryan, and I build stuff on the internet. I'm currently building Signal Genius.

I blog about my failed startup, MessageSling, at The Day Series.

Things I used to do:

  • Built and launched FourthSegment
  • Hacked at Punchbowl.com.
  • Founded MessageSling.com.
  • Spent several years at EMC

Me, Elsewhere

  • @angilly on Twitter
  • Facebook Profile
  • angilly on Flickr
  • angilly on Foursquare
  • My Skype Info
  • ryana on github

Twitter

loading tweets…

Following

I Dig These Posts

  • Photo via tmills

    A serious bath-taking bear.

    [via]

    Photo via tmills
  • Photo via tmills

    I read this thing on Vice tonight about how girls hate girls even when they’re friends and while all things women are forever hermetically sealed...

    Photo via tmills
  • Link via graysky
    cdixon.org – chris dixon's blog / Best practices for raising a VC round

    (via Instapaper)

    Link via graysky
  • Photo via dancroak

    Puppy.

    Photo via dancroak
See more →
  • RSS
  • Random
  • Archive
  • Ask me anything
  • Mobile

Effector Theme by Carlo Franco.

Powered by Tumblr