In Progress
Unit 1, Lesson 21
In Progress

Send Variations

Sometimes you need to invoke a method whose name you won’t know until runtime. In this episode, you’ll learn the differences between #send, #public_send, and #__send__, gotchas of using them, and when to use each one.

Video transcript & code

Ruby is an object-oriented language, and as such Ruby programs are made up of objects and method calls.

duck = Duck.new
duck.quack

Except in Ruby we tend to follow the Smalltalk tradition and refer to method invocation as "sending a message". Here, we’re sending the message "quack" to an object referred to by the variable "duck".

Most of the time the messages we send are hardcoded, as they are here. But occasionally we need to add some indirection. We need the message selector to be data instead of of code.

In this case we can use a symbol to represent the command we want to send to the object. Here we're deciding whether the goose should either honk or hiss at the gardener with a coin flip.

Once we have a message chosen, we send the goose object the send message. It's a little bit circular.

The first argument is the name of the actual message we want to send.

Any other arguments can follow.

goose = Goose.new
gardener = Human.new
message = [true, false].sample ? :honk : :hiss
goose.send(message, at: gardener)

OK, granted this is a silly example.

A more realistic case is a command-line utility that accepts subcommands as arguments. Here we have a goose program with a couple of subcommands.

$ goose honk
HONK HONK, feathermucker!
$ goose hiss
Scoundrel, I hiss at thee!

Each of the subcommands for this program is implemented as a method on the App class.

To translate command-line subcommands into method calls, we create a new App instance. Then we use send to interpret the command-line arguments as a message name, along with some arbitrary list of arguments.

#!/usr/bin/env ruby

class App
  def hiss(*)
    puts "Scoundrel, I hiss at thee!"
  end

  def honk(*)
    puts "HONK HONK, feathermucker!"
  end
end

App.new.send(*ARGV)

A drawback here is that this code makes any method on the App class, public or private, available as a subcommand !

For instance, since we happen to know that puts is implemented as a private method inherited from Kernel, we can get up to some sneaky tricks.

$ goose puts "quack quack I'm a duck"
quack quack I'm a duck

Let’s see if we can prevent this. Instead of send, we can use public_send. This behaves exactly like send, except that it’s restricted to only calling public methods.

when we try that puts stunt again, we get an error.

OK, problem solved! … right?

$ ./goose2 puts "quack quack I'm a duck"     
Traceback (most recent call last):
        1: from examples/goose2:13:in `<main>'
examples/goose2:13:in `public_send': private method `puts' called for #<App:0x00000000062e3e88> (NoMethodError)

Hmmm… you know what’s available as a public method on all objects? send.

$ goose2 send puts "quack quack I'm a duck"
quack quack I'm a duck

Heh. Oops. I guess that didn’t really lock things down after all, did it?

To prevent this and other problems, I like to use a convention where all the top-level methods that implement subcommands share a specific prefix. Like, do_hiss and do_honk.

Then we pull out the subcommand and other arguments from the command line.

And in the public_send, we look for a method named do_ followed by the subcommand the user specified.

This also illustrates another fact about the message sending methods: like many Ruby APIs, it will accept either a symbol or a string for the message name.

#!/usr/bin/env ruby

class App
  def do_hiss(*)
    puts "Scoundrel, I hiss at thee!"
  end

  def do_honk(*)
    puts "HONK HONK, feathermucker!"
  end
end

subcommand, args = *ARGV
App.new.public_send("do_#{subcommand}", *args)

This time when we try to be sneaky we get an exception.

But we can still invoke officially sanctioned subcommands like honk.

Now the only methods that can be attempted as subcommands are public ones that start with do_.

$ ./goose3 send puts "quack quack I'm a duck"
Traceback (most recent call last):
        1: from examples/goose3:14:in `<main>'
examples/goose3:14:in `public_send': undefined method `do_send' for #<App:0x00000000063f83c8> (NoMethodError)
$ ./goose3 honk
HONK HONK, feathermucker!

So now we have a handle on the send and public_send methods. But there's a very important third message-sending method to know,

and to understand why we need a new example.

Let’s say we’ve got a little mail-sending class.

We instantiate it with a default sender.

And then to send mail we use deliver with a message and a to.

For the sake of example, this currently just prints a message to STDOUT.

class MailSender
  def initialize(sender:)
    @sender = sender
  end

  def deliver(message, to:)
    send({from: @sender, to: to, body: message})
  end

  private

  def send(envelope)
    # ...
    puts "SENT to <#{envelope[:to]}>: #{envelope[:body]}"
  end
end

MailSender.new(sender: "goose@example.org").deliver("HONK!", to: "gardener@example.org")

# >> SENT to <gardener@example.org>: HONK!

Now let’s say we want to collect some performance benchmarks on the methods in this class.

And what’s more, let’s say we want to do it in a fancy way where we have a generic private benchmark method and we want to be able to wrap it around arbitrary other methods to collect data on them.

  private

  def benchmark(method_name)
    start_time = Time.now
    yield
    end_time = Time.now
    puts "#{method_name} executed in #{end_time - start_time}"
  end

Fortunately, we just so happen to have made a handy dandy module for adding method Advice.

(If you haven’t run into this term before, “advice” is a term of art for extra code hooked onto the beginning or end of a function or method.)

Once we’ve extended our class with the Advice module, we can use the class method around to wrap the deliver method with our benchmark method.

class MailSender
  extend Advice
  around :deliver, wrap: :benchmark
  #...
end

But when we try to exercise the deliver method, we get an exception.

# ~> ArgumentError
# ~> wrong number of arguments (given 2, expected 1)
# ~>
# ~> metaprogramming2.rb:46:in `send'
# ~> metaprogramming2.rb:14:in `block in method_added'
# ~> metaprogramming2.rb:52:in `<main>'

We trace the source of the exception to some code in the Advice module.

module Advice
  def around(subject_method, wrap:)
    (@@advice_wrappers ||= {})[subject_method] = wrap
  end

  def method_added(method_name)
    super if defined? super
    return if Thread.current[:advice_in_method_added]
    Thread.current[:advice_in_method_added] = true
    wrapper_name = @@advice_wrappers[method_name]
    if wrapper_name
      original = instance_method(method_name)
      define_method(method_name) do |*args, **kwargs, &block|
        send(wrapper_name, method_name) do
          original.bind(self).call(*args, **kwargs, &block)
        end
      end
    end
  ensure
    Thread.current[:advice_in_method_added] = nil
  end
end

We’re not going to go over this metaprogramming code in detail.

The problem line, however, has a familiar method in it: send. The exception says this method was called with the wrong number of arguments. That’s weird… since send just forwards to other methods, shouldn’t we be able to pass any number of arguments to send?

We discover the key to this little puzzle when we look through the private code of the MailSender class, and discover that it has an internal support method named… send.

def send(envelope) # ~> ArgumentError: wrong number of arguments (given 2, expected 1)
    # ...
    puts "SENT to <#{envelope[:to]}>: #{envelope[:body]}"
end

When the Advice module tries to dynamically send a *Ruby * message, it instead winds up invoking this mail-sender-specific send method. Oops.

See, here’s the thing. Having a method named send to dynamically send messages to objects is one of those language design choices that seemed like a good idea at the time, but did not age well. It turns out that there are lots of perfectly legitimate business-domain reasons to want to name a method send, like sending emails or sending HTTP requests or sending printer control codes. And of course, Ruby being Ruby, it’s not going to stop you from overriding a system method.

Let’s go back to the Advice module. What can we do to fix this? We can’t change it to public_send, because it needs to be able to invoke private methods like our benchmark wrapper.

Fortunately, Ruby has an alternative.

We can change the send invocation to __send__ (from here on out I’m just going to call this “dunder send”). __send__ is just an alias for send. But unlike the no-underscore version, it has a weird, ugly name that no one is likely to accidentally override.

Now when we test out the code, we can see benchmark output accompanying the method invocation.

So, what’s the takeaway here? Fortunately it boils down to some pretty straightforward guidelines.

If you need to be able to invoke a method and you don’t know the name of the method until runtime, always start with public_send.

In some cases, usually involving metaprogramming, you may need access to private methods as well. In this case, replace public_send with __send__.

As for the original-flavor send method… just don’t use it. Ever. Think of it as deprecated.

And don’t be shy about naming your own methods send if that name makes sense for them. Well-written Ruby libraries will use one of the other dynamic invocation methods, and you won’t break them by hiding the system send method.

obj.public_send(method_name, args...) # use by default
obj.__send__(method_name, args...)    # when you need private access
obj.send(method_name, args...)        # just don't

And that completes our tour of the send family of methods. Happy hacking!

Responses