开发者

Why do migrations need the table block param?

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 :co开发者_StackOverflowl
     integer :col2
     integer :col3
end

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


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.


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.


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
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜