As a completely random exercise a month or two ago, I sat down and hacked out a very simple implementation of the Javascript with keyword for Ruby. with basically works like this:

with(object)
{
    instanceMethod(); // no need to prefix with `object'
    var test = 5;
    other_instanceMethod(5);
}

So basically it temporarily scopes function calls to a given object. If you call something in there that isn’t an instance method, it’ll then look for a local function with the appropriate name (note that this is contrary to the assertion I originally made in the aforementioned implementation; I’ve since verified that this is the expected behavior).

Initially, I just passed the block on to instance_eval and happily went my way. It was a satisfactory solution, but not an ideal one, because, as pointed out after the snippet, it failed to respect encapsulation. Moreover, if you called something in there that wasn’t an instance method, it would fail. Now, after being spurred on by my brother’s mention of a friend’s solution that involved proxy objects, I’ve got an implementation which does not have those problems and in fact seems to largely behave the same way as the Javascript construct it is based on.

Without too much further ado, here’s the full implementation:

require 'pp'
module With
  def with(obj = nil, &block)
    raise ArgumentError, 'Block expected.' unless block_given?

    # Collect instance methods from the block's original binding so we can pass
    # them on to the proxy object, since otherwise the +instance_eval+ will lose
    # them.
    instance_method_names = eval('methods + private_methods + protected_methods',
                                 block.binding)
    instance_methods = {}
    instance_method_names.each do |meth|
      instance_methods[meth.to_sym] = eval("self.method('#{meth}')", block.binding)
    end

    proxy = WithProxy.new(obj || self, instance_methods)

    # Inject instance variables from the block's original binding into the proxy
    # object, since otherwise the +instance_eval+ will lose them.
    instance_vars = eval('instance_variables', block.binding)
    instance_vars.each do |var|
      proxy.instance_variable_set var, eval(var, block.binding)
    end

    proxy.instance_eval &block
  end

  class WithProxy
    # Kill all instance methods except __send, __id, and +instance_eval+ (which
    # we need in order to use this as a proxy in a with block.
    instance_methods.each do |meth|
      undef_method(meth) unless meth =~ /^__/ || meth == 'instance_eval' ||
        meth == 'instance_variable_set'
    end

    # Initializes a proxy object that will first try proxying methods to the
    # public methods of the passed +obj+ parameter, and, if that doesn't work,
    # try proxying to any of the +additional_methods+. +additional_methods+
    # should be a Hash indexed by the method name as a symbol.
    def initialize(obj, additional_methods)
      @obj = obj
      @additional_methods = additional_methods
    end

    def method_missing(method, *args)
      if method_allowed?(method)
        @obj.send(method, *args)
      elsif @additional_methods.include?(method)
        @additional_methods[method].call(*args)
      else
        raise NoMethodError, "undefined or inaccessible method #{method} for #{@obj}" 
      end
    end

    # Determines if the specified method is allowed to be passed on.
    def method_allowed?(method)
      methods_allowed[method] ||= @obj.class.public_instance_methods.include?(method.to_s)
    end

    def methods_allowed
      @methods_allowed ||= {}
    end
  end
end

There are a few fixes floating around here to previous problems. First and foremost, it should be clear that, instead of using instance_eval on the object that we’ll be dealing with directly, we instead use a proxy object. For that purpose, we have the WithProxy class. This class has all of its instance methods undef’ed except for the __ methods (send and id), instance_eval (for obvious reasons), and instance_variable_set (for reasons we’ll get to in a second). This proxy object lets us make sure to respect encapsulation by only allowing public methods to be callable.

To this end, this implementation of with uses method_missing to catch method calls, and checks whether they can be proxied to the original object. The method_allowed? method checks whether a given method is in the list of a class’s public methods, and memoizes the result so that further calls to that method don’t induce the overhead of searching the list of public methods every time.

The second piece of the puzzle is the @additional_methods instance variable in the proxy object. This is a hash that maps method names (as symbols) to Method objects. It’s used to specify additional methods that are allowed beyond the public instance methods in the original object. If you’ll recall, the behavior of with in Javascript is to look for functions in the enclosing scope when no instance methods are found with the given name. This list of additional methods lets us do that. If a method is not allowed to be called on the given object, then the list of additional methods is searched and, if a matching method is found, that method is called.

Notice also that the list of additional methods is passed in to the WithProxy’s constructor in the with method, and it consists of all public, private, and protected methods in the enclosing scope of the passed block (retrieved using block.binding and eval). These are essentially all the methods that should be available without qualification within that block.

Finally, we also have to do a similar trick with instance variables. We again use eval and block.binding to nab instance variables available in the enclosing scope and inject them into the proxy object. Usually, instance variables are available in the context of a block, since a block is a closure; however, in this case, because we’re using instance_eval, the instance context of the block changes, so that it’s executing in a different object instance than the block was originally in. Thus, we lose both the original instance methods and the original instance variables. This rather hackish fix takes care of that.

So, that said, is it worth using? I dunno. The added complexity hanging around here isn’t necessarily nice to incur in an application. Maybe it’s worth using in a short script for the purposes of saving a few keystrokes, but in general I’d say it’s just an interesting mental exercise in what’s possible in Ruby when you really put your mind to it.

You can also grab the original source file and the related spec.

2 Responses to “A Largely Functional Implementation of `with' for Ruby”

  1. Will Says:

    Nice implementation, I haven’t done any serious Javascript coding so that concept of a ‘with’ was new to me. I typically think of with more along the Common Lisp lines “(with-socket sock udp 9421)”, and then you can refer to socket within that lexical closure, and the socket will be automatically closed once you leave that scope.

    As for the value of your with, I think that its more likely to create confusing code (which Ruby is already fully capable of with its syntax variations ;), rather than be a good tool. Take that with a grain of salt, I am wrong very frequently.

    Keep up the good entries. And on a slight side note, does your blog not have an RSS feed? That would be worth your time to implement (its a lot easier to check a newsreader than to remember to wander over to sites, and a lot faster too ;).

  2. Shadowfiend Says:
    It actually does have a feed -- http://blog.withoutincident.com/feed/ , as provided out of the box by Mephisto; however, there's no direct link here. There's a reference to it (an actual HTML link tag), though, so if your browser looks for those it should be detecting it. Nonetheless, I think I'll add a direct link to it in the sidebar :-)

    To address your concerns about it making things confusing—yeah, you’re probably right. Like I said, it was more of a question of `I wonder if I can do this…’ I haven’t really found myself missing the with functionality often. But I was happy that it was not only doable, but in a fairly simple way.

    Ah, Common Lisp… Still currently on my `kind-of-barely-know-it’ list :D

Sorry, comments are closed for this article.