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

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

Пример:

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

Ответы

Ответ 1

Мой любимый пример метапрограммирования, подобный этому, - это динамическое построение метода, который вы собираетесь использовать в цикле. Например, у меня есть механизм запросов, который я написал в 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

Но если ваши рабочие наборы могут стать большими, вы делаете это выражение большого дела 1000 или 1000000 раз для каждой операции, даже если он будет принимать одну и ту же ветку на каждой итерации. В моем случае логика гораздо более активна, чем просто аргумент 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

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

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

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

Ответ 2

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

Ответ 3

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

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

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() заключается в том, чтобы документировать, используя описательные (хотя и не очень хорошие в этом придуманном примере), что эти части делают. Но если у вас есть много методов, подобных этому, код в 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() завернуты в методы, чтобы поддерживать логику самого балла() на том же уровне абстракции, не вмешиваться в хэши и т.д.

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

Итак, имея эту структуру, вы всегда можете прочитать всю реализацию оценки(), не глядя на какой-либо другой метод, определяемый вне оценки Scoring #. Несмотря на то, что я часто не вижу такой код 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)

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

Ответ 4

Не использовать 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. Конструкция def не использует блок, поэтому он больше не закрывается над переменной name.

Конструкция def не позволяет вам "передавать" любую информацию для параметризации определения определяемого вами метода. Это делает его бесполезным в динамическом контексте. Нет смысла определять метод с помощью def из метода. Вы всегда можете перенести одну и ту же внутреннюю конструкцию def из внешнего def и в конечном итоге тем же способом.


Кроме того, определение методов динамически имеет стоимость. Ruby кэширует ячейки памяти методов, что повышает производительность. Когда вы добавляете или удаляете метод из класса, Ruby должен выкинуть этот кеш. (До Ruby 2.1 этот кэш был глобальным. По состоянию на 2.1, кеш-класс является классом.)

Если вы определяете метод внутри другого метода, каждый раз, когда вызывается внешний метод, он делает недействительным кеш. Это отлично подходит для макросов верхнего уровня, таких как attr_reader и Rails 'belongs_to, потому что все они вызываются при запуске программы, а затем (надеюсь) никогда больше. Определение методов во время текущего выполнения вашей программы немного замедлит вас.

Ответ 5

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