2

I'm trying to do a DSL, in which the user can pass a block and expect an instance variable @arg to be defined. This is a full example with a unit test failing:

# 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

Using pry or putting puts statements inside the block, I can see that @arg is nil. But Foo.new.instance_variable_get :@arg correctly outputs foo, so it must be related to some scoping rules.

What do I need to change in the implementation to make the test pass and the DSL to work?

4
  • @Stefan The @arg of the class in which I'm calling filter. How could I tweak the implementation then so the @arg in the block gets evaluated in the scope of the instantiated class? Commented May 28, 2019 at 10:43
  • 1
    @Stefan: filter is an instance method. filters, which sets up filter, is a class method. However, filters captures a block in a class context and passes it on to filter; OP wishes to have the block executed in the instance context. (Not saying it's possible or impossible just yet, my brain hurts.) Commented May 28, 2019 at 11:07
  • @Amadan Stefan nailed it in the last comment, that's my problem exactly. I need a class method for filters so I can create the DSL in the post (which is part of a bigger project), or at least I think I do. The end objective here is to tweak the implementation so the test passes for the example filter Foo that I have created. Commented May 28, 2019 at 11:15
  • @Amadan oh I see, I thought filter and filters were the same. I missed the define_method call. Commented May 28, 2019 at 11:44

1 Answer 1

3

instance_exec to rescue!

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"]

Caution: Make sure this is very well documented, since any shenanigans involving instance_exec or its cousins are breaking programmer expectations left and right - by design, you're destroying the concept of "scope". I'm pretty sure OP knows this, but it is worth putting down on the proverbial paper.

Also, consider using accessors rather than plain instance variables - accessors are checked, and variables are not. i.e. { |el| el == urg } will result in an error, but { |el| el == @urg } will silently fail (and filter for nil).

Sign up to request clarification or add additional context in comments.

1 Comment

You are right about the accessors, I'll do that. Thanks!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.