Переменная экземпляра в блоке при использовании define_method

Я пытаюсь сделать DSL, в котором пользователь может передать блок и ожидать, что будет определена переменная экземпляра @arg. Это полный пример с ошибкой модульного теста:

# Implementation
class Filter
  def initialize
    @arg = 'foo'
  end

  def self.filters &block
    define_method :filter do |els|
      els.select &block
    end
  end
end

# Usage
class Foo < Filter
  filters {|el| el == @arg}
end

# Expected behavior
describe 'filters created with the DSL' do
  subject { Foo.new }
  it 'can use @arg in the filters block' do
    els = %w[notthearg  either  foo  other]
    expect(subject.filter els).to be_eql(['foo'])
  end
end

Используя pry или помещая операторы puts внутрь блока, я вижу, что @arg равно нулю. Но Foo.new.instance_variable_get :@arg правильно выводит foo, поэтому это должно быть связано с некоторыми правилами области видимости.

Что мне нужно изменить в реализации, чтобы тест прошел и DSL заработал?

@Stefan @arg класса, в который я звоню filter. Как мне тогда настроить реализацию, чтобы @arg в блоке оценивался в рамках созданного класса?

RedPointyJackson 28.05.2019 12:43

@Stefan: filterявляется метод экземпляра. filters, который устанавливает filter, является методом класса. Однако filters захватывает блок в контексте класса и передает его filter; OP хочет, чтобы блок выполнялся в контексте экземпляра. (Пока не говорю, что это возможно или невозможно, мой мозг болит.)

Amadan 28.05.2019 13:07

@ Амадан Стефан прибил это в последнем комментарии, это точно моя проблема. Мне нужен метод класса для filters, чтобы я мог создать DSL в посте (который является частью более крупного проекта), или, по крайней мере, я так думаю. Конечная цель здесь — настроить реализацию так, чтобы тест проходил для примера фильтра Foo, который я создал.

RedPointyJackson 28.05.2019 13:15

@ Амадан, о, понятно, я думал, что filter и filters одно и то же. Я пропустил звонок define_method.

Stefan 28.05.2019 13:44
Пошаговое руководство по созданию собственного Slackbot: От установки до развертывания
Пошаговое руководство по созданию собственного Slackbot: От установки до развертывания
Шаг 1: Создание приложения Slack Чтобы создать Slackbot, вам необходимо создать приложение Slack. Войдите в свою учетную запись Slack и перейдите на...
1
4
136
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

instance_exec спасать!

class Filter
  def initialize
    @arg = 'foo'
  end

  def self.filters &block
    define_method :filter do |els|
      els.select { |e| self.instance_exec(e, &block) }
    end
  end
end

class Foo < Filter
  filters {|el| el == @arg }
end

Foo.new.filter(%w[notthearg  either  foo  other])
# => ["foo"]

Предостережение: убедитесь, что это очень хорошо задокументировано, поскольку любые махинации с участием instance_exec или его двоюродных братьев нарушают ожидания программиста направо и налево — по замыслу вы разрушаете концепцию «области действия». Я почти уверен, что ОП знает об этом, но это стоит записать в пресловутую бумагу.

Кроме того, рассмотрите возможность использования средств доступа, а не простых переменных экземпляра — средства доступа проверяются, а переменные — нет. т. е. { |el| el == urg } приведет к ошибке, но { |el| el == @urg } завершится ошибкой (и отфильтрует nil).

Вы правы насчет аксессуаров, я сделаю это. Спасибо!

RedPointyJackson 28.05.2019 13:46

Другие вопросы по теме