Ruby Did You Know That: #3 (For Loops and Fields)

Posted by -

A while ago I came across an interesting bit while handling variable bindings with Ruby for loops, namely that you can actually use a Constant as a for loop variable.

Now I'm working on developing a control flow graph for the annotated AST, and discovered another feature of for loop variables: you can use field syntax (also called accessor syntax):

  Person = Struct.new(:name, :address)
  Address = Struct.new(:street, :city)

  my_house = Address.new('West St.', 'Hanover')
  shared_house = Address.new('Sachem Village', 'Lebanon')
  mike = Person.new('Mike', my_house)
  alice = Person.new('Alice', shared_house)
  bob = Person.new('Bob', shared_house)

  for mike.name, bob.address.city in [['James', 'Omaha'], ['Mike', 'Lebanon']]
    mike.address.city, christine.address.city = christine.address.city, mike.address.city
  end

What are the values of mike.address.city, alice.address.city, and bob.address.city after that for loop runs? I hope I'm not the only one who finds this nontrivial!

The confusing bit is a result of the fact that we're running extra code and mutating objects in the actual for loop assignment. Really, just two methods Person#name= and Name#city= are being called, but also bob.address is another method call during the assignment, and it could even itself do anything confusing such as cause a mutation! For example, if instead we defined Person#address as:

  Person = Struct.new(:name, :address) do
    def address
      (@counter ||= 0) += 1
      address.street *= counter
      super
    end
  end

Now each iteration of the above loop mutates the incoming variable differently with each iteration of the loop, in the middle, non-assigning method!

It was easy to say I thought constants didn't belong in the for loop variable list. This is slightly different as it might make sense for some reason, perhaps with mock objects in a test running loop. It still seems disastrous to dance with. Laser, the static analyzer and linter that is the subject of my undergraduate thesis, will warn if it discovers Ripper :field nodes are discovered in the a loop variable list.

Personal Opinion: Mutation

This seems like a point where I might note that a recent discussion on Ruby-Talk (379527) touched on, and one which Laser will have to make a choice: to what convention should Rubyists assign the use of bang (!) at the end of a method name? The standard library is fraught with inconsistencies, with expert advice suggesting it primarily indicate in-place mutation, but also sometimes indicating irreversible actions, and return values (gsub! comes to mind) are also often inconsistent. Some have suggested that ! should be used to indicate mutation in new code, with others dissenting in favor of it indicating general dangerousness.

I personally think knowing precisely which methods cause a mutation lexically, namely if they end in = or !, can be a useful convention. I think the fact that I could modify the address when the bob.address portion of bob.address.city= executed indicates disaster if the method I had called did some other mutation.

I'll be adding to Laser a simple regex setting that must pass on mutating method names, with a quick option for deactivation from the command-line. It will default to /[!=]$/.


Enjoy this article? Then feel free to:


Comments