Ruby's Object#send allows dynamic calling of a method. This 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)
# 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
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])
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)
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)
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]
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]
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]
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,
# wrong, does not work like Object#send
a.rsend([:slice, 2, 8]) # right
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
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.
Recent Comments