Forum: Ruby Recursive send

Announcement (2017-05-07): www.ruby-forum.com is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see rubyonrails.org/community and ruby-lang.org/en/community for other Rails- und Ruby-related community platforms.
Curtis S. (Guest)
on 2007-01-19 17:31
(Received via mailing list)
I posted this to the Dallas Ruby Brigade, but thought it might be
interesting for the larger Ruby audience.  Your comments, questions,
and complete debunking of why something like this is even necessary are
welcomed.

Curtis S.
---------

Ruby's Object#send is very useful, but what if we wanted to call
several levels deep on an object?  For instance:

# Normal call chain
post.comments.first.commented_at

# Dynamically with send?  Have to call three times.
post.send(:comments).send(:first).send(:commented_at)


What if the number of calls to send is variable depending on what we're
trying to show?  In one case we might need post.posted_at for the date,
and in another case we might need post.comments.first.commented_at for
the date.

How could we dynamically craft the definition of the methods to send if
we don't know how many calls to Object#send we'll have?  We need a way
to define an arbitrary number of method calls.

Behold, a recursive send:  Object#rsend

class Object
 def rsend(*args, &block)
   obj = self
   args.each do |a|
     b = (a.is_a?(Array) && a.last.is_a?(Proc) ? a.pop : block)
     obj = obj.__send__(*a, &b)
   end
   obj
 end
 alias_method :__rsend__, :rsend
end

Each argument passed to Object#rsend is an array with the symbols and
arguments that will be passed on to Object#send:

post.rsend([:comments],[:first],[:commented_at])

If there are no arguments to be passed on to send, the array brackets
can be omitted:

post.rsend(:comments, :first, :commented_at)


Of course, in practice you'll probably be defining your method call
chain in one part of your code, putting it in a variable, and sending
it to rsend with a splat*:

the_date = [:comments, :first, :commented_at]

#...somewhere else in your code you've passed the_date along:
post.rsend(*the_date)


With arguments:

a = [0,1,2,3,4,5,6,7,8,9]

a.rsend([:slice, 2, 8]) #=> [2, 3, 4, 5, 6, 7, 8, 9]

a.rsend([:slice, 2, 8], [:slice, 1, 3]) #=> [3, 4, 5]


Object#send accepts a block.  What about blocks?  Pass in a proc:

a.rsend([:map, (proc { |x| x*2 })])
 #=> [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

a.rsend([:map, (proc { |x| x*2 })],
       [:select, (proc { |x| x % 4 == 0})])
 #=> [0, 4, 8, 12, 16]

And, in an effort to make Object#rsend behave like Object#send for the
simple case, you can send a regular block:

a.rsend(:map) { |x| x*2 }
 #=> [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

Caveat:  For the case needing parameters, Object#rsend does require an
array, so:

a.rsend(:slice, 2, 8) # wrong, does not work like Object#send

a.rsend([:slice, 2, 8]) # right

A quirk that I've left in for fun, but it might (and maybe should)
change:  If providing a single block, that block will be called on
every call unless you've already passed in a proc:

a.rsend(:map, :map) { |x| x*2 }
 #=> [0, 4, 8, 12, 16, 20, 24, 28, 32, 36]

a.rsend(:map, [:map, (proc { |x| x+5 })], :map) { |x| x*2 }
 #=> [10, 14, 18, 22, 26, 30, 34, 38, 42, 46]
 #outer block was called on first and third :map

Can anyone come up with a good use for this call-the-block-each-time
behavior?

Has anyone done this already?  I searched for such a thing and came up
empty.  Maybe this method should be called something else?  I named it
based on each call recursing down the chain of methods with a new
object being returned for the next method to be sent to.

Suggestions and comments are welcome.

-------------
The original blog post:
http://www.csummers.org/index.php/2007/01/18/ruby-...
Daniel DeLorme (Guest)
on 2007-01-22 03:27
(Received via mailing list)
Curtis S. wrote:
> # Dynamically with send?  Have to call three times.
> post.send(:comments).send(:first).send(:commented_at)
>
>
> What if the number of calls to send is variable depending on what we're
> trying to show?  In one case we might need post.posted_at for the date,
> and in another case we might need post.comments.first.commented_at for
> the date.

A word of warning: I once thought of using that same functionality in
order
to post complex information from an html form, e.g.
  <textarea name="blog[text]"></textarea>
  <select name="blog[text.format]">
    <option>raw html</option>
    <option>textile</option>

But I then realized that was a major security hole. It allows an
attacker
to post stuff like:
  <input name="blog[connection.drop_database.something]"
By the time the recursive send fails on "something=", the database has
already been wiped. Well, this example doesn't really work
(drop_database
requires an argument), but you get the idea.

Daniel
Curtis S. (Guest)
on 2007-01-22 17:26
(Received via mailing list)
> But I then realized that was a major security hole. It allows an attacker
> to post stuff like:
>   <input name="blog[connection.drop_database.something]"
> By the time the recursive send fails on "something=", the database has
> already been wiped. Well, this example doesn't really work (drop_database
> requires an argument), but you get the idea.

Well, if you are going to send an unescaped, form submitted value to
rsend, then, yes, that would be a security hole.  But that's kind of
like saying you're going to allow an unescaped, client submitted value
to eval--which would be silly.

My usage of this is more along the lines of:

RoR controller w/ several actions that will render the same view.  The
date that I want to show in that view might be one of several choices
of variable method depth depending on the action being rendered.  So,
in each action I set the appropriate method call chain to pass to
rsend, and then use that variable in the view.

Here's a contrived example:

#controller
def action1
  @posts.find(:all, :include => :comments)
  @use_this_date = [:posted_at]
  render :template => 'posts/list'
end

def action2
  @posts.find(:all, :include => :comments)
  @use_this_date = [:comments, :first, :commented_at]
  render :template => 'posts/list'
end

#view
<% @posts.each do |post| %>
  <%= h post.title %>,
  <%= h post.rsend(*@use_this_date) %>
<% end %>
Daniel DeLorme (Guest)
on 2007-01-23 10:06
(Received via mailing list)
Curtis S. wrote:
> to eval--which would be silly.
Heheh. True to a certain extent, but while sending an unescaped string
to
eval is obviously crazy, send seems safer. After all, RoR relies on
stuff
like <input name="obj[field]" absolutely all over the place. So it may
not
be immediately apparent that <input name="obj[field.subfield]" is far
more
dangerous.

But on the original topic, my own implementation of rsend was more like
this:
  class Object
    def rsend(msg, *others)
      result = send(msg)
      result = result.rsend(*others) unless others.empty?
      result
    end
  end
This allows a class to override the rsend method in order to provide
specific
behavior, e.g. in case send(msg) returns nil.

Daniel
This topic is locked and can not be replied to.