DSL, select и системные методы

April 11, 2012

При работе с руби иногда встречаются ошибки, которые вызывают замешательство. Недавно я создавал простой DSL для своей библиотеки на остове метода instance_eval (метод весьма спорный, его нужно использовать аккуратно, но сегодня речь не об этом). Интерпретатор руби вылетал с очень странной ошибкой, на простом коде:

class Dsl
  def initialize(&block)
    @fields = []
    instance_eval(&block)
  end

  METHODS = %w[select text string]

  def method_missing(method, *args, &block)
    if METHODS.include?(method.to_s) && args.size == 1
      @fields << [method, args]
    else
      super
    end
  end
end

Dsl.new do
  string :a
  select :b  # <--- BOOM!
end

Трейс ошибки:

demo.rb:20:in `select': wrong argument type Symbol (expected Array) (TypeError)
    from demo.rb:20:in `block in <main>'
    from demo.rb:4:in `instance_eval'
    from demo.rb:4:in `initialize'
    from demo.rb:18:in `new'
    from demo.rb:18:in `<main>'

Сейчас меня эта ошибка не удивляет, так как я знаю в чем подвох. Но тогда я очень удивился, даже безуспешно пытался отладить свой код.

Секрет очень простой - в руби есть системный метод select (Kernel.html#method-i-select) и происходит конфликт имен. Хорошо, что я еще не "угадал" аргументы, тогда бы ошибка была бы еще сложнее.

Обойти эту проблему очень просто, необходимо объявить метод select в DSL явно:

  # ...

  METHODS = %w[text string]

  def select(name)
    @fields << [:string, name]
  end

  def method_missing(method, *args, &block)
  # ...

Таким образом при работе с рейлс у вас в распоряжении минимум 3 различных метода select:

  • хелпер метод для рисования комбобоксов;
  • метод поиска по массиву ([1, 2, 3, 4, 5].select{|a| a%2 == 0});
  • и наш сегодняшний таинственный незнакомец.

Рейлс, как и само руби, довольно плохо защищено от конфликтов имен, так как количество системных методов руби и внутренних методов рейлс равно бесконечности. Поэтому если вам кажется, что интерпретатор свихнулся, возможно вы как раз поймали ошибку аналогичную описанной.

И еще один совет - не используйте класс Config в глобальном пространстве имен:

# OK
module A
  class Config
  end
end

# BAD
class Config # <-- in `<main>': Config is not a class (TypeError) 
end
comments powered by Disqus