A Curious Use of For Loops In Ruby

I was thinking about the use of for loops in Ruby the other day, triggered by one of the Ruby problems over at rubeque.

Now, a for-loop in Ruby usually looks something like this:

for i in collection
  # i takes each value that's in the collection
end

Interesting things happen when the loop variable (i in the case above) is assigned to a value already before the loop. In particular, interesting things can happen if it’s not even a variable.

Let’s see what one can do with a for-loop in Ruby (or rather to a for-loop). Enter pry (or your favourite Ruby-REPL [Read-Evaluate-Print-Loop]):

pry(main)> for Object.new in [1,2,3]; end
NoMethodError: undefined method `new=’ for Object:Class
from (pry):3:in `block in __pry__’

Oh, that’s interesting: The interpreter does not complain about Object.new not being a variable, but rather that there’s not method ‘new=’ for class Object.

OK. let’s play with this… Let’s assume a class Foo:

class Foo
  def initialize foo
    @foo = foo
  end

  def foo= other
    @foo.unshift other
  end
end

Note that there’s no method named foo for class Foo, just ‘foo=’. Initialising a variable now with, say, an empty Array, there’s a curious way to reverse an Array:

pry(main)> f = Foo.new(res)
=> #<foo:0x007fd392c02368 @foo=[]>
pry(main)>
pry(main)> for f.foo in [:first, 2, 'third']
pry(main)* end
=> [:first, 2, "third"]
pry(main)> pp res
["third", 2, :first]
=> ["third", 2, :first]

There is a way to use an empty loop to reverse an Array. (Note. this does not solve the problem given at rubeque, since a class definition is not allowed inside a method definition). In fact there are more ways than this!

A few new things I learned about for loops in Ruby:

  1. They do not require a variable name as the ‘loop variable’
  2. If one uses what looks like a method call (the ‘f.foo’ in the example above) as the  ‘loop variable’ then…
    1. …the object f refers to, doesn’t even need to respond to that method, but
    2. …instead it needs to respond to ‘method_name=’ (‘foo=’ in the example above).

I don’t know what to think about this, but you probably shouldn’t rely on this behaviour or use it in production code.

Advertisement

Pasting Code Is OK

While reading about the DRY principle (for “Don’t repeat yourself”) and the evil of copy-and-paste coding (again), I started thinking what to do instead. Actually, what to do is more or less obvious: Put the code into a place where its accessible to be reused — a method, may be in a new or existing module or class.

So, whenever I feel the ‘need’ to copy code, I now think about cutting it, creating a new method and calling that. Apart form avoiding duplication, the code is now testable immediately by calling the method (instead of getting the surrounding code exercised). And the methods using the code gets shorter.

In the end, it boils down to pasting code being perfectly OK, it’s the copying that causes the trouble.

The Method you’re in

Sometimes I like to find out in which method my programs are currently in — may be for debugging but a lot more likely to log that fact for later analysis. For some (long running) automation stuff that’s really useful. Especially if (or when) the exact order of method calls it not deterministic. Think for example of a rules based engine which randomly applies a set of rules to a set of objects.

That said, you could just make each method log it:

def method_1 
  logger.info "Executing #{ method_1 }" 
  # more code 
end

But that’s not DRY after the second method doing that. And it’s manual, cumbersome and error-prone.

Well, there’s Kernel#caller, but I was looking for something a bit more compact. So here it comes:

module MethodCallList 
  def method_name 
    list = caller[ 0..-2 ] 
    if list.empty? 
      "method_name" 
    else 
      list.map do | el | 
        if md = el.match( /in '(.+)'/ ) 
          md[ 1 ] 
        else 
          '' 
        end 
      end.reverse.join( '/' ) 
    end 
  end 
end

Is there any better, more elegant or completely different way to do this in Ruby? I’d love to hear about that.
Hmm, the module name could be better than this…