@Object#send@ – the joy of Rubyists and the scourge of those who would write refactoring tools. Let’s talk about it.
I recently ended up writing some code that would have proven really hideous if I couldn’t call @#send@. So I had a class like the following (written using code generation for brevity):
class Foo
%w{one two three}.each do |name|
class_eval <<-EOC
def #{name}
'#{name}'
end
EOC
end
end
I needed to call @one@, @two@ or @three@ from a bit of code that takes some user input (in this case, an attribute value in a template language). So I cooked something like this up:
%w{one two three foo}.each do |arg|
meth = case arg
when 'one'
:one
when 'two'
:two
when 'three'
:three
else
raise 'Unknown arg'
end
puts Foo.new.send(meth)
end
So I’m using a case statement to convert _valid_ method names to symbols, which I then pass to @#send@ and *bam!*, my method is called. This made my code a ton easier to write and I’m here espousing the technique to you.
But what is the drawback? Well, if someone were to ever really get up the courage to tackle the task of building a refactoring browser for Ruby, this sort of thing would give them fits. They can’t really tell where those methods are called on my class until they are actually called. Heck, given the code above, they can’t even figure out what methods exist on @Foo@ without running the code.
The other drawback is that this code is a little hard to read. Most new casual Rubyists won’t think to search for the symbol version of a method name. Its even harder if you’re still somewhat new to Ruby and you aren’t aware this sort of thing even happens.
For me personally, the concise code I can write with @#send@ far outweighs the drawbacks. I preach the importance of code reading regularly, and its how one can get over the “hump” that is knowing how to navigate software that sends dynamic messages to objects.
Your homework is to share how you feel about @#send@ in the comments.
If you’ve already got the verbosity of the case statement, #send is entirely unjustified. Just remove the colon from the symbol names to call the methods directly.
Of course, the entire case statement is unnecessary to begin with; something this trivial shouldn’t take more than two lines:
raise “Unknown arg” if ![‘one’, ‘two’, ‘three’].include?(arg)
puts Foo.new.send(meth)
Three lines would be acceptable if you want to factor out a VALID_ARGS constant and check against that instead of inlining it.
In most cases, I think I’d rather have my object be the one to decide what valid arguments were, since that’s part of it’s domain. So I’d add a method on Foo, like…
def do_it(do_what)
…case or whatever to break out to method…
end
my $0.02. greg.
Are you aware of the #to_sym method on String? I believe (as Phil pointed out above), #send works with a string or a symbol, so there’s not really a need for the case statement. However, let’s pretend you had to pass a symbol to #send.
puts Foo.new.send(meth.to_sym)
I’m fine with your send, but your case creeps me out. How about:
raise ‘Unknown Arg’ unless %W(one two three).include? arg
puts Foo.new.send(arg)
(send can take a string as the message name.)
Let’s pretend we didn’t see that. (don’t worry, I won’t tell anybody)
OK, ya’ll are right that I could stand to use an @Array.include?@ rather than a case statement.
@Greg, I have mixed feelings about methods doing dispatch in that way. But maybe I’m the pot calling the kettle black.