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