Ruby: имеет ли определение метода внутри другого метода какое-либо реальное использование?

Я читал статью о мета-программировании, и она показала, что вы можете определить метод в другом методе. Это то, что я знал некоторое время, но это заставило меня задать себе вопрос: имеет ли это какое-либо практическое применение? Существует ли какое-либо реальное использование определения метода в методе?

Ex:

def outer_method
  def inner_method
     # ...
  end
  # ...
 end

5 ответов


мой любимый пример метапрограммирования-это динамическое построение метода, который вы собираетесь использовать в цикле. Например, у меня есть механизм запросов, который я написал в Ruby, и одна из его операций-фильтрация. Существует множество различных форм фильтров(подстрока, equals,=, пересечения и т. д.). Наивный подход таков:

def process_filter(working_set,filter_type,filter_value)
  working_set.select do |item|
    case filter_spec
      when "substring"
        item.include?(filter_value)
      when "equals"
        item == filter_value
      when "<="
        item <= filter_value
      ...
    end
  end
end

но если ваши рабочие наборы могут стать большими, Вы делаете это заявление большого случая 1000s или 1000000s раз для каждого операция, даже если она будет принимать одну и ту же ветвь на каждой итерации. В моем случае логика намного сложнее, чем просто оператор case, поэтому накладные расходы еще хуже. Вместо этого, вы можете сделать это так:

def process_filter(working_set,filter_type,filter_value)
  case filter_spec
    when "substring"
      def do_filter(item,filter_value)
        item.include?(filter_value)
      end
    when "equals"
      def do_filter(item,filter_value)
        item == filter_value
      end
    when "<="
      def do_filter(item,filter_value)
        item <= filter_value
      end
    ...
  end
  working_set.select {|item| do_filter(item,filter_value)}
end

теперь разовое ветвление выполняется один раз, спереди, и результирующая одноцелевая функция используется во внутреннем цикле.

на самом деле, мой реальный пример делает три уровня этого, так как есть вариации в интерпретации обоих рабочий набор и значение фильтра, а не только форма фактического теста. Поэтому я создаю функцию item-prep и функцию filter-value-prep, а затем создаю функцию do_filter, которая использует их.

(и я действительно использую lambdas, а не defs.)


Да, есть. На самом деле, я готов поспорить, что вы используете хотя бы один метод, который определяет другой метод каждый день: attr_accessor. Если вы используете рельсы, в постоянном использовании есть еще тонна, например belongs_to и has_many. Это также обычно полезно для конструкций в стиле AOP.


Я думаю, что есть еще одно преимущество использования внутренних методов, которые касаются ясности. Подумайте об этом: класс со списком методов-это плоский, неструктурированный список методов. Если вы заботитесь о разделении проблем и сохранении материала на одном уровне абстракции, а фрагмент кода используется только в одном месте, внутренние методы приходят на помощь, сильно намекая, что они используются только в методе замыкания.

предположим, что у вас есть этот метод в a класс:

class Scoring
  # other code
  def score(dice)
    same, rest = split_dice(dice)

    set_score = if same.empty?
      0
    else 
      die = same.keys.first
      case die
      when 1
        1000
      else
        100 * die
      end
    end
    set_score + rest.map { |die, count| count * single_die_score(die) }.sum
  end

  # other code
end

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

class Scoring
  # other methods...
  def score(dice)
    same, rest = split_dice(dice)

    set_score = same.empty? ? 0 : get_set_score(same)
    set_score + get_rest_score(rest)
  end

  def get_set_score(dice)
    die = dice.keys.first
    case die
    when 1
      1000
    else
      100 * die
    end
  end

  def get_rest_score(dice)
    dice.map { |die, count| count * single_die_score(die) }.sum
  end

  # other code...
end

идея get_set_score () и get_rest_score () заключается в документировании с помощью описательного (хотя и не очень хорошего в этом придуманном примере), что делают эти части. Но если у вас много методов, как это, кода в результат() не так легко следовать, и если вы рефакторинг либо из методов вам может понадобиться, чтобы проверить, какие другие методы использует их (даже если они частные - другие методы того же класса могут использовать их).

вместо этого я начинаю предпочитать это:

class Scoring
  # other code
  def score(dice)
    def get_set_score(dice)
      die = dice.keys.first
      case die
      when 1
        1000
      else
        100 * die
      end
    end

    def get_rest_score(dice)
      dice.map { |die, count| count * single_die_score(die) }.sum
    end

    same, rest = split_dice(dice)

    set_score = same.empty? ? 0 : get_set_score(same)
    set_score + get_rest_score(rest)
  end

  # other code
end

здесь должно быть более очевидно, что get_rest_score () и get_set_score () обернуты в методы, чтобы сохранить логику score () на том же уровне абстракции, без вмешательства с хэшами и т. д.

обратите внимание, что технически вы можете вызов Scoring#get_set_score и Scoring#get_rest_score, но в этом случае это будет плохой стиль IMO, потому что семантически они являются просто частными методами для одного метода score ()

Итак, имея эту структуру, вы всегда можете прочитать всю реализацию score (), не глядя на какой-либо другой метод, определенный вне Scoring#score. Хотя я не часто вижу такой Ruby-код, я думаю, что пойду чтобы преобразовать больше в этот структурированный стиль с помощью внутренних методов.

Примечание: еще один вариант, который не выглядит чистым, но избегает проблемы столкновений имен, - просто использовать lambdas, который был в Ruby с самого начала. Используя пример, он превратится в

get_rest_score  = -> (dice) do
  dice.map { |die, count| count * single_die_score(die) }.sum
end
...
set_score + get_rest_score.call(rest)

это так же красиво - кто-то, глядя на код, может задаться вопросом, почему все эти лямбды, в то время как использование внутренних методов довольно самодокументировано. Я все еще склоняюсь к тому, чтобы просто ... lambdas, поскольку у них нет проблемы утечки потенциально конфликтующих имен в текущую область.


не используя def. для этого нет практического применения, и компилятор, вероятно, должен вызвать ошибку.

есть причины динамически определять метод во время выполнения другого метода. Считать attr_reader, который реализован в C, но может быть эквивалентно реализован в Ruby как:

class Module
  def attr_reader(name)
    define_method(name) do
      instance_variable_get("@#{name}")
    end
  end
end

здесь мы используем #define_method определить способ. #define_method является фактическим методом;def нет. Это дает нам два важное свойство. Во-первых, он принимает аргумент, который позволяет нам передать его name переменная для имени метода. Во-вторых, он принимает блок, который закрывается над нашей переменной name позволяет нам использовать его изнутри определения метода.

так что произойдет, если мы используем def вместо?

class Module
  def attr_reader(name)
    def name
      instance_variable_get("@#{name}")
    end
  end
end

это вообще не работает. Во-первых,def ключевое слово сопровождается буквальным именем, а не выражением. Это означает, что мы определяем метод с именем, буквально, #name, чего мы совсем не хотели. Во-вторых, тело метода ссылается на локальную переменную name, но Ruby не распознает его как ту же переменную, что и аргумент #attr_reader. The def construct не использует блок, поэтому он не закрывается над переменной


Я думал о рекурсивной ситуации, но я не думаю, что это будет иметь достаточный смысл.