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