Ruby : to __callee__ or not __callee__

moltenhead

Charlie Gardai

Posted on September 26, 2023

Ruby : to __callee__ or not __callee__

Ever met those instructions :

  • __FILE__
  • __method__
  • __callee__

?

I think we all met at least the first one while playing with Ruby within our early experimentations ... and maybe used the second one for debugging processes - if so I propose you to try a debugger like pry, byebug, pry-byebug you'll get lots of the information right away (if you are using Ruby >3.1 you even get an integrated one).
But do you know what does the last one ?

The difference with method

First of all, let's identify what __method__ is doing :

def world
  p "hello #{__method__}"
end

world
Enter fullscreen mode Exit fullscreen mode

Executing this file will produce:

$ > hello world
Enter fullscreen mode Exit fullscreen mode

But why ?

Simply put : __method__ is returning as a symbol the name of the method the instruction is in.
Here the method is world, so it puts the interpolation of the string and the method symbol which gives : hello world.

So what is the difference with __callee__ ?
Well __callee__ is returning the actual name of the called method dynamically as a symbol.

Let's example ourselves

class Foo
  def root_method
    p "you did #{__method__}"
  end

  def root_callee
    p "you did #{__calle__}"
  end
end

Foo.new.tap do |this|
  this.root_method
  this.root_callee
end
Enter fullscreen mode Exit fullscreen mode

File execution :

$ > you did root_method
  > you did root_callee
Enter fullscreen mode Exit fullscreen mode

Here nothing big differentiate both behaviours : both return their method name.

Let's then put the __callee__ specificities to the test :

class Foo
  alias_method :aliased_method, :root_method
  alias_method :aliased_callee, :root_callee
end

Foo.new.tap do |this|
  this.aliased_method
  this.aliased_callee
end
Enter fullscreen mode Exit fullscreen mode

File execution :

$ > you did root_method
  > you did aliased_callee
Enter fullscreen mode Exit fullscreen mode

So what happened ?

  1. The aliased method that uses __method__ interpolates the root method name.
  2. The aliased method that uses __callee__ interpolates with the aliased method name.

__callee__ instead of __method__ is looking up for the actual method name, not the one it originated from !

Ok ... but how can I use this ?

That's a good question !

Let's imagine a situation where we send automatic mailing to update an asynchronous task for our users .

We will always send the same data structure, and the only thing that will change is the corpus of the mail.
We could go the naive way and produce a Mailer with every specific methods we need for every possible situations. But that's tedious and imply a lot of repetition - which we don't want !

So how can we use __callee__ here to simplify our day to day life ?

A one Mailer to rule them all

So we need a Mailer that will always send emails with 3 parameters : email, user_name, a job_id, to notify an asynchronous event to the users.
The only things that will change depending on the event is :

  1. the subject of the mail
  2. the corpus of the mail

So first we need something that will be able to structure the needed data as we need it :

class AsynchronUpdateMailer < ActionMailer::Base
  private

  def default_mail_action(email:, user_name:, jid:)
    @email = email
    @name = user_name
    @jid = jid

    mail(
      to: ["#{@name} <#{@email}>"],
      subject: "#{__callee__.to_s.gsub('_', ' ')}: #{jid}",
      &:html
    )
  end
end
Enter fullscreen mode Exit fullscreen mode

We now have an AsynchronUpdateMailer that is able to receive parameters to structure a default email and uses __callee__ and the job id to structure the subject. That's Good !
But we can't do anything with it yet.
We turn the method private because this should never get called : no need to send a default mail, what we need is a simple first structure to be able to quickly extend its behaviour with the less possible code

So let's extend it a bit to turn it a little bit more usefull :

class AsynchronUpdateMailer < ActionMailer::Base
  VALID_ACTIONS = %i[
    foo_success
    foo_failure
    bar_success
  ]

  VALID_ACTIONS.each do |valid_alias|
    alias_method valid_alias, :default_mail_action
  end

  private
  [...]
end
Enter fullscreen mode Exit fullscreen mode

What did we do here ?

  1. We defined a constant that identify which actions this mailer is able to produce.
  2. when this Mailer is loaded by Rails, it will programmatically define an alias for every "valid action" that aliases default_mail_action, and each of those aliases will be public

Ok ... that's good and all. But what about the corpus ? What is the interest of this proposition if everyone of the sent emails does the same ?
Well, the answer is simple : we're going to use the amazing powers of Rails views system to resolve our situation !

Rails views to the rescue

Every time a ActionMailer::Base uses the mail method it will rely on the called method name and the application file structure to obtain the view that matches :

  1. the Mailer name to find the folder within the app/views folder
  2. the given action name to find the corresponding .haml file

So let's add our files where they need to be !
Within the app/views folder we create a folder that is te machine name of our Mailer : app/views/asynchronous_update_mailer. We then create a .haml for every action we need - corresponding to the aliases we defined :

  • foo_success.haml
  • foo_failure.haml
  • bar_success.haml

Each one of them is a common haml file and icing on the cake : we event get access to the state of the Mailer within this haml file.
So we can do things like :

%html
  %body
    %section
      %p
        = "Hello #{@user_name}!"
      %p
        = "You can use the number given within the subject of this mail to contact the support if need be !"
      ...
Enter fullscreen mode Exit fullscreen mode

So ? Can you feel the maintenability and scalability of this setup tickling your brain ?
Obviously you can always go one step further, but here we potentially saved a lot of time and maintenance using the power of Rails !

Conclusion

__callee__ is one of those tool Ruby proposes that we don't always need ... but can still be of great use in the right context.
It always comes down to what we have and what we do with it. I'm sure you already made great use of it and can share with us in the comment some of your experiences with its usage !

That's it for today !

Thank you for reading this article, and I hope it got you something interesting out of it. At least I had a blast typing it.

I'm sure you already made great use of the __callee__ method, so don't hesitate to give your insights and personal examples in the comments : they will be greatly appreciated.

Enjoy your skills and have a great day !

💖 💪 🙅 🚩
moltenhead
Charlie Gardai

Posted on September 26, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related