C
Curtis Summers
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 Summers
---------
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.sendcomments).sendfirst).sendcommented_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.rsendcomments, :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.rsendmap) { |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.rsendslice, 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.rsendmap, :map) { |x| x*2 }
#=> [0, 4, 8, 12, 16, 20, 24, 28, 32, 36]
a.rsendmap, [: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.
interesting for the larger Ruby audience. Your comments, questions,
and complete debunking of why something like this is even necessary are
welcomed.
Curtis Summers
---------
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.sendcomments).sendfirst).sendcommented_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.rsendcomments, :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.rsendmap) { |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.rsendslice, 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.rsendmap, :map) { |x| x*2 }
#=> [0, 4, 8, 12, 16, 20, 24, 28, 32, 36]
a.rsendmap, [: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.