Ruby: Recursive send

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)

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.

1 comments

Do you have an idea folder?

I have an idea folder. It’s just a folder with a bunch of text files in it. Each text file contains an idea. Some ideas are potential business ideas, others are programming projects that I think would be useful, others are book/article ideas. If it’s an idea, it goes in the idea folder.

I’ve only been doing this for about six months, and in that time I’ve managed to come up with ideas to keep me busy working for several years!

Do you have an idea folder?

4 comments

OpenDNS

Every so often our cable internet (Time Warner Road Runner) will go “out.” Only it’s not really “out”: the cable modem’s internet connection is still on and I can ping IP addresses. What’s really happening is Road Runner’s DNS servers stop responding to DNS queries. This doesn’t happen very often, and it is usually resolved in a few minutes, but that’s too long when I’m trying to work.

The solution: OpenDNS. These guys run a top-notch DNS service. It’s free, reliable, fast, and supposedly safer. The Getting Started page has detailed instructions for using OpenDNS’s servers in place of your ISP’s servers.

1 comments

I recently worked on a project using the CakePHP web framework. One problem I had was placing the framework in a subdirectory of the main website and accessing the application without a trailing slash:

This worked:

http://mysite.com/sub/

This did not:

http://mysite.com/sub

Usually Apache figures out that “sub” is a directory and will redirect to the slashed location for you. However, CakePHP’s default mod_rewrite rules play havoc with this and the result was a 400 Bad Request error. The solution is relatively simple, but was not intuitively easy to figure out.

CakePHP ships with this .htaccess file in the base CakePHP directory:

<IfModule mod_rewrite.c>
  RewriteEngine  on
  RewriteRule ^$  app/webroot/     [L]
  RewriteRule (.*) app/webroot/$1  [L]
</IfModule>

In my attempts to solve this problem, I had tried several rewrite rules and redirects from within this file and from within my website’s root web directory (one above the CakePHP directory), but only the following worked for me:

1.) Put this in your website root directory (where “sub” is your CakePHP directory):

<IfModule mod_rewrite.c>
  RewriteEngine  on
  RewriteRule ^sub$       sub/app/webroot/     [L]
  RewriteRule ^sub/(.*)$ sub/app/webroot/$1  [L]
</IfModule>

2) AND, DELETE OR RENAME your .htaccess file in the CakePHP sub directory.

Hope that helps someone.

9 comments

Ruby on Rails and Ruby are an amazing combo. Here’s another example of why.

ActionView’s TextHelper methods are useful, but I often need to use them in my controller or my model. For several of the TextHelper methods that expect a string as input, it makes sense to extend the String class.

So, if I want to strip HTML tags, auto link any URLs, and then simple format a comment (in that order) before I save it in the database I can do:

class Comment < ActiveRecord::Base
#...
  def before_save
    self.text = self.text.strip_tags.auto_link.simple_format
  end
end

This method is much cleaner than including ActionView::Helpers::TextHelper in whatever class I’m in and passing the string as an argument to each method.

Below is the magic code. Since TextHelper is a module, we create a Singleton class to reference the methods, create the wrapper methods in their own module, and finally include that module in the String class. Note that not all TextHelper methods are included–just the ones that make sense. Drop this code into a file and require it in your environment or within a plugin.

# ActionView Text Helpers are great!
# Let's extend the String class to allow us to call
# some of these methods directly on a String.
# Note:
#  - cycle-related methods are not included
#  - concat is not included
#  - pluralize is not included because it is in
#       ActiveSupport String extensions already
#       (though they differ).
#  - markdown requires BlueCloth
#  - textilize methods require RedCloth
# Example:
# "<b>coolness</b>".strip_tags -> "coolness"
 
require 'singleton'
 
# Singleton to be called in wrapper module
class TextHelperSingleton
  include Singleton
  include ActionView::Helpers::TextHelper
  include ActionView::Helpers::TagHelper #tag_options needed by auto_link
end
 
# Wrapper module
module MyExtensions #:nodoc:
  module CoreExtensions #:nodoc:
    module String #:nodoc:
      module TextHelper
 
        def auto_link(link = :all, href_options = {}, &block)
          TextHelperSingleton.instance.auto_link(self, link, href_options, &block)
        end
 
        def excerpt(phrase, radius = 100, excerpt_string = "...")
          TextHelperSingleton.instance.excerpt(self, phrase, radius, excerpt_string)
        end
 
        def highlight(phrase, highlighter = '<strong class="highlight">\1</strong>')
          TextHelperSingleton.instance.highlight(self, phrase, highlighter)
        end
 
        begin
          require_library_or_gem 'bluecloth'
 
          def markdown
            TextHelperSingleton.instance.markdown(self)
          end
        rescue LoadError
          # do nothing.  method will be undefined
        end
 
        def sanitize
          TextHelperSingleton.instance.sanitize(self)
        end
 
        def simple_format
          TextHelperSingleton.instance.simple_format(self)
        end
 
        def strip_tags
          TextHelperSingleton.instance.strip_tags(self)
        end
 
        begin
          require_library_or_gem 'redcloth'
 
          def textilize
            TextHelperSingleton.instance.textilize(self)
          end
 
          def textilize_without_paragraph
            TextHelperSingleton.instance.textilize_without_paragraph(self)
          end
        rescue LoadError
          # do nothing.  methods will be undefined
        end
 
        def truncate(length = 30, truncate_string = "...")
          TextHelperSingleton.instance.truncate(self, length, truncate_string)
        end
 
        def word_wrap(line_width = 80)
          TextHelperSingleton.instance.word_wrap(self, line_width)
        end
 
      end
    end
  end
end
 
# extend String with the TextHelper functions
class String #:nodoc:
  include MyExtensions::CoreExtensions::String::TextHelper
end

This idea and code was somewhat inspired by Gabriel’s post on using helpers inside a controller. Thanks Gabriel!

9 comments