Explain Ruby's define_method like I'm five

Y'all I'm dying here. Can somebody please explain how this works?

Did you find this post useful? Show some love!
DISCUSSION (7)

Hi Tiffany!

define_method does what the name entails. Allows you to create a method (instance method) on the fly. Let's take a step back.

If you were a five year old I would probably use a videogame analogy: the powerups. You're Mario or Zelda or whatever and you are running through the game. You end up picking up a mushroom or a tool and you're (let's ignore the fact that these powerups are usually temporary) able to do something new that you weren't five seconds ago. Basically the videogame added, while you were already playing, a new skill to your player (and only yours if you were playing a multi player game). You can use this skill like if it were there from the beginning, even if you know it was not.

define_method is like one of those super powers in videogames: allows you to add functions (skills) to your object (a singular player).

A quick side note: most examples on the internet use define_method for a tangential (but still powerful) usage: shortening the amount of code you have to type. For example: let's say you have a Job object that can be in a few different states: created, queued, running, completed, failed and you want to ask the job if it's running. You might decide to use define_method to iterate over all the possible states (likely defined in constants or a hash) and create methods so you can do job.running?.

I think the true power of define_method is in the analogy with the videogame though, not just to let the developer write less code during the definition of the class.

Let's see some code, shall we?

Let's start from the side note, adding methods based on a series of states:

class Job
  STATES = [:queued, :running, :completed, :failed]

  attr_accessor :status

  STATES.each do |method_name|
    define_method "#{method_name}?" do
      status == method_name
    end
  end
end

job = Job.new
p "job.queued? #{job.queued?}"
job.status = :queued
p "job.queued? #{job.queued?}"

This prints:

"job.queued? false"
"job.queued? true"

Following the game analogy here we're still a little off, because if you look closely we defined the method inside the class Job which means that ANY job will have those methods.

Let's take it a step further:

class Player
  attr_reader :name

  def initialize(name)
    @name = name
  end

  def walk
    p "#{name}: walking"
  end

  def run
    p "#{name}: running"
  end
end

mario = Player.new("mario")
luigi = Player.new("luigi")

mario.walk
luigi.run

This will print:

"mario: walking"
"luigi: running"

So, how we give a powerup to Mario but not to luigi? The standard library comes to our rescue:

class Player
  attr_reader :name

  def initialize(name)
    @name = name
  end

  def walk
    p "#{name}: walking"
  end

  def run
    p "#{name}: running"
  end

  # this allows you to add any method to a single instance at runtime
  def create_method(name, &block)
    self.class.send(:define_method, name, &block)
  end
end

mario = Player.new("mario")
luigi = Player.new("luigi")

mario.walk
luigi.run

mario.create_method(:fly) do
  p "#{name}: flying like an eagle!"
end

mario.fly

This will print:

"mario: walking"
"luigi: running"
"mario: flying like an eagle!"

There's still an issue, if we were to inadvertantly ask Luigi to fly this would happen:

"luigi: fly like an eagle!"

Wait, what? The thing is that define_method operates on the class by default. What if we really want this to happen just for mario?

This is where I get lost in Ruby metaprogramming (I admit I never fully understood this syntax):

class Player
  attr_reader :name

  def initialize(name)
    @name = name
  end

  def walk
    p "#{name}: walking"
  end

  def run
    p "#{name}: running"
  end

  def create_method(name, &block)
    (class << self; self; end).instance_eval do
      define_method(name, &block)
    end
  end
end

mario = Player.new("mario")
luigi = Player.new("luigi")

mario.walk
luigi.run

mario.create_method(:fly) do
  p "#{name}: fly like an eagle!"
end

mario.fly
luigi.fly

this will print:

"mario: walking"
"luigi: running"
"mario: fly like an eagle!"
Traceback (most recent call last):
t.rb:34:in `<main>': undefined method `fly' for #<Player:0x00007fa7c0950cf8 @name="luigi"> (NoMethodError)

As you can see only mario now has that method. Fortunately there's a clearer way to create this "method generator", define_singleton_method:

class Player
  attr_reader :name

  def initialize(name)
    @name = name
  end

  def walk
    p "#{name}: walking"
  end

  def run
    p "#{name}: running"
  end

  def create_method(name, &block)
    self.define_singleton_method(name, block)
  end
end

mario = Player.new("mario")
luigi = Player.new("luigi")

mario.walk
luigi.run

mario.create_method(:fly) do
  p "#{name}: fly like an eagle!"
end

mario.fly
luigi.fly

Ruby metaprogramming is definitely a complicated part of the language. :-)

Thank you for this amazing, detailed response! It is complicated but you definitely made it much clearer. :)

In the latter case (extending the object with new functionality,) the proper way to go would be to Object#extend:

mario.extend(Module.new do
  def fly
    p "#{name}: flying like an eagle!"
  end
end)

Also, in Ruby2 we don’t need (class << self; self; end) magic anymore; use singleton_class instead:

mario.singleton_class == (class << mario; self; end)
#⇒ true

@mudasobwa thanks for the addition!

Do you agree how weird is that syntax though?

I have to call extend on an object but then suddenly have to create a new Module and put my method in it.

Ruby's metaprogramming syntax is quite obscure sometimes.

Compare it with Python's, which I find more explicit and clear in this case:

import types

class Player:
  def __init__(self, name):
    self.name = name

  def walk(self):
    print(f"{self.name}: walking")

  def run(self):
    print(f"{self.name}: running")

  def create_method(self, name, method):
    # set an attribute on this istance, with a name and the given method
    setattr(self, name, types.MethodType(method, self))


mario = Player("mario")
luigi = Player("luigi")

mario.walk()
luigi.run()

def fly(self):
  print(f"{self.name}: fly like an eagle!")
mario.create_method('fly', fly)

mario.fly()

luigi.fly()

the output:

mario: walking
luigi: running
mario: fly like an eagle!
Traceback (most recent call last):
  File "t.py", line 29, in <module>
    luigi.fly()
AttributeError: 'Player' object has no attribute 'fly'

How is it obscure? In Ruby you cannot have a method on its own (well, you can have an UnboundMethod, but this is out of scope here.)

Having an anonymous module owning a method is a correct approach. It’s not a matter of taste, it’s plain right and clean.

There is a mixin (might be anonymous,) there is an object to extend (no matter whether it’s an instance, a class, or an eigenclass.) One mixes the mixin in (sorry.)

One might prepend a mixin to the eigenclass of an object and call super from there—the very handy OO behaviour that python lacks at all. I find Ruby metaprogramming very clear and consistent. The better example would be Elixir only (and maybe erlang to some extent.)

Having an anonymous module owning a method is a correct approach. It’s not a matter of taste, it’s plain right and clean.

I'm not saying it's wrong or bad, it's obviously right in the context of how Ruby works. I'm just saying it seems weird if you read it.

To add a method named fly to an object mario I have to extend with a method inside an anonymous module that's going to be "injected" inside mario.

In Python this reads: to add a method named fly to an object mario I have to attach it to mario.

This is what I meant with "I find more explicit and clear".

Any way you put it metaprogramming is super cool, in each language I come across of it :-)

Thanks for the reminder about Elixir, I need to get around it sooner or later.

Classic DEV Post from Oct 18

dev.to Show us your octocat alter ego

Show us your octocat alter ego

Tiffany Wismer
I was an Admin Assistant and a Barista and now I'm learning code because I'm tired of talking to people. Just kidding. But also not kidding.
Join dev.to