Inject outer scope when monkey-patching instance method in Ruby [duplicate] - ruby

This question already has answers here:
Is it possible to define a Ruby singleton method using a block?
(2 answers)
Closed 7 years ago.
I have a container class Foo with a method frob, and I want to add a similarly named method, which will delegate to the container, to each of the contained elements.
First I tried
self.children.each do |c|
def c.frob
self.frob
end
end
but this of course leads to SystemStackError: stack level too deep, as self is c at that point. I then tried
parent = self
self.children.each do |c|
def c.frob
parent.frob
end
end
but local variables aren't part of the closure of the newly defined method, so I get undefined local variable or method 'parent'.
I came up with the following hack, which works:
self.children.each do |c|
c.instance_variable_set('#parent', self)
def c.frob
#parent.frob
end
end
However, it pollutes the variable space of the child with something that's only needed by this one method. How can I get parent/self in there while keeping the newly defined method self-contained?

This should work:
children.each do |c|
parent = self
c.send(:define_method, :frob) do
parent.frob
end
end

Related

Ruby: Working with instance variables within a case statement [duplicate]

This question already has answers here:
Why do Ruby setters need "self." qualification within the class?
(3 answers)
Closed 2 months ago.
I'm perplexed as to why I need to selectively refer to an instance variable with "self" inside a case statement inside a class method:
I have a class with an instance method #working_dir:
class FileSystem
attr_accessor :sizes, :working_dir
attr_reader :input
def initialize(input)
#input = input.split("\n")
#sizes = Hash.new(0)
#working_dir = []
end
...
end
I've defined a method parse_cmd that performs an operation on #working_dir depending on the outcome of a case statement:
...
def parse_cmd(str)
cmd_arr = str.split(' ')
return unless cmd_arr[1] == 'cd'
case cmd_arr[2]
when '..'
working_dir.pop
when '/'
self.working_dir = ['/']
else
working_dir << cmd_arr[2]
end
end
...
Rubocop/the interpreter yells at me if I exclude the self on self.working_dir = ['/']. Why is this? Why do I need to include it here, but not on other references to #working_dir within the case statement?
Consider a simple example:
class A
attr_accessor :b
def initialize(b)
#b = b
end
def c
b = 42
end
end
a = A.new(27)
# => #<A:0x00007f7999088bc0 #b=27>
a.c
# => 42
a.b
# => 27
Calling a.c is assigning 42 to a local variable b, and is not modifying the instance variable #b.
I'd either need to use self.b = 42 or #b = 42 to ensure I am modifying the instance variable.
In your case, you don't need to use self.working_dir elsewhere because those uses cannot be construed as assigning to a local variable. Because no local variable working_dir exists, the accessor method is used.
In your case statement, you are NOT directly refering to the #working_dir instance variable. Instead, you are using the accessor methods defined by attr_accessor :working_dir at the top of your class.
When calling attr_accessor, it will effectively define two methods on your class:
def working_dir
#working_dir
end
def working_dir=(value)
#working_dir = value
end
This allows you to access the value of the instance variable via the method call as if it were a local variable (but it's not, it's always a method call).
Now, in order to call the setter method working_dir=, Ruby requires that you call it with an explicit receiver (self in your case).
This is because without an explicit receiver, if you assign some value, Ruby will always assign to local variable. With working_dir = 'value', you are thus always creating a local variable named working_dir and assign a value to it.
If you use an explicit receiver however, e.g. self.working_dir = 'value', Ruby knows that this can not be a variable assignment anymore and will thus call your setter method.

Why does ruby treat variables differently inside 'def' and 'define_method'? [duplicate]

I would like to understand how define_method works and how to properly use the variables outside of the definition block. Here is my code:
class Test
def self.plugin
for i in 1..2
define_method("test#{i}".to_sym) do
p i
end
end
end
plugin
end
ob = Test.new
ob.test1 #=> 2 (I would expect 1)
ob.test2 #=> 2 (I would expect 2)
It seems that in the methods test1 and test2, the value of i is not substituted during the definition, but is computed directly on the spot when the method is called. So we see only the latest value of i, which is 2. But where does Ruby take this value from? And is there a way to let test#{i} print i?
In this particular case, I could do a workaround using __method__, but probably there is a better solution.
As mentionned in comments this is down to closures - the block passed to define_method captures the local variables from its scope (and not just their value as you found out).
for doesn't create a new scope, so your method 'sees' the change to i. If you use a block (for example with each) then a new scope is created and this won't happen. You just need to change your code to
class Test
def self.plugin
(1..2).each do |i|
define_method("test#{i}".to_sym) do
p i
end
end
end
plugin
end
which is basically what the question linked by Arup was about

Implementation of attr_accessor [duplicate]

This question already has answers here:
Why do Ruby setters need "self." qualification within the class?
(3 answers)
Closed 5 years ago.
Excuse me for the noob question.Please explain me outputs of the below ruby programme for implementing attr_accessor.
class SimpleService
attr_accessor :name
def initialize(name)
#name = name
end
def process
if false # some condition met
name = 'Akshay'
end
name
end
end
When I execute this class
SimpleService.new('John Doe').process
=> nil
Why is the result nil?
when I use self explicitly to name
def process
if false # some condition met
self.name = 'Akshay'
end
name
end
Now the output is
SimpleService.new('John Doe').process
=> "John Doe"
why is the result now "John Doe"?
I am a beginner in ruby.
Thanks in advance!
The thing is when you call name = you implicitly declare new local variable. Try this:
def process
name = 'Akshay'
puts local_variables.inspect
end
Why is it that way is a complicated question, discussed many times there and here. The setter always requires in explicit receiver. Period.
Once you have the line name = 'Akshay' inside a method, you introduce a new local variable and this method’s scope gets extended with new local variable name, despite how is was declared. It’s basically done by ruby parser.
And local variables take precedence over instance methods. That is why what is returned in the last line is a local variable. That was apparently not set, due to falsey condition above. Hence nil.

Using local variables in define_method

I would like to understand how define_method works and how to properly use the variables outside of the definition block. Here is my code:
class Test
def self.plugin
for i in 1..2
define_method("test#{i}".to_sym) do
p i
end
end
end
plugin
end
ob = Test.new
ob.test1 #=> 2 (I would expect 1)
ob.test2 #=> 2 (I would expect 2)
It seems that in the methods test1 and test2, the value of i is not substituted during the definition, but is computed directly on the spot when the method is called. So we see only the latest value of i, which is 2. But where does Ruby take this value from? And is there a way to let test#{i} print i?
In this particular case, I could do a workaround using __method__, but probably there is a better solution.
As mentionned in comments this is down to closures - the block passed to define_method captures the local variables from its scope (and not just their value as you found out).
for doesn't create a new scope, so your method 'sees' the change to i. If you use a block (for example with each) then a new scope is created and this won't happen. You just need to change your code to
class Test
def self.plugin
(1..2).each do |i|
define_method("test#{i}".to_sym) do
p i
end
end
end
plugin
end
which is basically what the question linked by Arup was about

How do I make a Ruby method that lasts for the lifetime of a block?

Inside the body of a class, I'd like to pass a block to a method called with. For the lifetime of the block, I would like a with_value method to be available.
Otherwise, everything inside the block should behave as if it were outside the block.
Here's an example:
class C
extend M
with "some value" do
do_something_complicated
do_something_complicated
do_something_complicated
end
end
We can almost get this with:
module M
def with(str, &block)
Object.new.tap do |wrapper|
wrapper.define_singleton_method :with_value do # Here's our with_value
str # method.
end
end.instance_eval &block
end
def do_something_complicated # Push a value onto an
(#foo ||= []).push with_value # array.
end
end
but there's a problem: since we're evaluating the block passed to with inside the context of a different object, do_something_complicated isn't available.
What's the right way to pull this off?
This will make with_value available only within the block. However, _with_value will be defined within or outside of the block.
module M
def _with_value
...
end
def with(str, &block)
alias with_value _with_value
block.call
undef with_value
end
...
end
I cannot tell from the question whether this is a problem. If it is a problem, you need to further describe what you are trying to do.
Basically, the idea is to use method_missing to forward method calls from the dummy class to the calling class. If you also need to access instance variables, you can copy them from the calling class to your dummy class, and then back again after the block returns.
The Ruby gem docile is a very simple implementation of such a system. I suggest you read the source code in that repository (don't worry, it's a very small codebase) for a good example of how DSL methods like the one in your example work.
Here is a way that is closer to your attempt:
module M
def with(str, &block)
dup.tap do |wrapper|
wrapper.define_singleton_method :with_value do
...
end
end.instance_eval &block
end
...
end
dup will duplicate the class from where with is called as a class method.

Resources