Posts Tagged ‘not so dynamic after all’

Idiomatic Ruby

Sunday, June 22nd, 2008

In my last post about python, I included the following ruby code to give an example of how to get a list of all the classes named “Test.*” but not “TestSlow”:

fastTests = []
ObjectSpace.each_object( Class ) { |x| fastTests << x.to_s if x =~ /^Test/ }
fastTests.delete(”TestSlow”) 

But this is ugly. The Ruby Way(tm) should be to do it in one line, right? Surely there is some idiomatic way to filter a collection — .select should do it.

But you can’t apply .select to the result of each_object(). In fact the return vale of each_object is a fixnum, not an array, and you only have access to the object list through the block.

But you can’t return a value out of a block, either. You can alter existing variables in scope, but the routine you passed the block to gets to determine its own return value.

I spent a little while hacking on a way around this limitation using exceptions as an additional return mechanism. It was evil and it didn’t work.

So I created the following function that executes a method which takes a block parameter and returns the results of iterating it as an array –


def unpack( method, args )
  a  = []
  method.call( args ) { |x| a << x}
  return a
end

With that in hand, you can get the available classes into an array, then filter them like this:

  fastTests = unpack( ObjectSpace.method( :each_object ), Class ).select do |x|
    # starts with test but is not testSlow
    x.to_s =~ /^test/ and not x.to_s == "testSlow"
  end


Or in many other clever ruby ways, surely.

But why isn’t that functionality built in? If you don’t pass an argument to each_object, it will iterate over all the currently active objects, which could yield a very large array. Forcing the user to use a block instead of blindly dumping the object references into an array is probably an attempt to be kind to the user, to prevent his shooting himself in the foot. But it yields the decidedly un-rubylike code that starts off this entry, with the explicit initialization of the target array.

By the way, I checked to see how it’s done in Test::Unit, written by someone who’s at least ten times more expert in ruby than I am, and it’s the same idiom:

        def collect(name=NAME)
          suite = TestSuite.new(name)
          sub_suites = []
          @source.each_object(Class) do |klass|
            if(Test::Unit::TestCase > klass)
              add_suite(sub_suites, klass.suite)
            end
          end

Maybe you could come up with something evil by creating a custom filter class and overriding the === method — which is how the each_object method is filtering what gets passed to the block. But it’s a little disappointing that there’s no simple builtin way to turn an iterator into a list.