views:

88

answers:

3

Why does the ruby on rails migration syntax look like this:

create_table :my_table do |t|
     t.integer :col
     t.integer :col2
     t.integer :col3
end

And not:

create_table :my_table do
     integer :col
     integer :col2
     integer :col3
end

Personally I find the second snippet much more readable, are there any reasons why the implementation uses the first?

A: 

My understanding is that ruby is lexically scoped, meaning that "integer" has to refer to something defined at the point it occurs in the code. You'd need dynamic scoping to accomplish what you're asking for.

It may be that I'm wrong and at least one of procs, blocks and lambdas is dynamically scoped, but then you still have your answer -- obscure details of how scope behaves is not a good thing to expect a programmer to know.

Kevin Peterson
I'm not sure how this answers the question.
Peter Wagenet
The mere presence of a block defines a scope... you could access it in create_table and give integer context to that table. I don't see why not...
Jamie Rumbelow
You can bring the block (more or less) into the scope where it's called with `instance_eval`.
Chuck
You could also simply define integer, string etc.. before the yield and undef them at the end.
Sam Saffron
+3  A: 

The fundamental implementation of the two approaches is different. In the first (and actual) case, create_table calls yield with a TableDefinition object. So t in your example block points to that TableDefinition. The alternative method is to use instance_eval. This would look something like:

def create_table(name, &block)
  table_definition = TableDefinition.new
  # Other setup
  table_definition.instance_eval(&block)
  # More work
end

Which way you do it is partially a matter of preference. However, some people are not fans of eval so they like to avoid this. Also, using the yield method makes it clearer what object you're working with.

Peter Wagenet
You could use a technique like mixico github.com/coderrr/mixico and avoid the self remapping and instance_eval. Nonetheless, I do not follow why you would need access to self during your column defs.
Sam Saffron
I do however think its peoples blanket dislike of instance_eval that lead to that decision, so will +1 you for that.
Sam Saffron
I'm sort of on the fence myself. I am drawn to the cleanness of the instance_eval option, but at the same time I get the feeling that maybe it's a good idea to avoid it. It's something I'll need to think about more.
Peter Wagenet
A: 

Basically the interface designer should have chosen to do so due to this little trick regarding scopes and how eval and instance_eval work, check this example:

Having 2 classes Foo and Boo with the following definitions:

class Foo
  def speak(phrase)
    puts phrase
  end 
  def self.generate(&block)
    f = Foo.new
    f.instance_eval(&block)
  end
end

class Boo
  attr_reader :name
  def initialize(name) ; @name = name ; end
  def express
    Foo.generate { speak name}
  end
end

Generally this should work fine for most cases, but some situations like the following statement will issue an error:

Boo.new("someone").express #`express': undefined local variable or method `name' for #<Foo:0xb7f582fc> (NameError)

We don't have access here to the instance methods of Boo inside Foo instances that's because we are using instance_eval, so the method name which is defined for Boo instances is not in the scope for Foo instances.

To overcome such problems it’s better to redefine generate as follows:

class Foo
  def speak(phrase)
    puts phrase
  end
  def self.generate(&block)
    f = Foo.new
    block.arity < 1 ? f.instance_eval(&block) : block.call(f)
  end
end

This is a flexible interface where you evaluate the code block depending on the passed block params. Now we have to pass the current foo object as a param when we need to call instance methods on it, let's redefine Boo, check express and talk:

class Boo
  attr_reader :name
  def initialize(name) ; @name = name ; end
  def express
    Foo.generate { |f| f.speak name}
  end
  def talk(anything)
    Foo.generate { speak anything}
  end
end

Boo.new("someone").express #=> someone
Boo.new("someone").talk("whatever") #=> whatever
khelll