Ruby variable from outer scope undefined within a block - why? - ruby

This part of code dynamically creates several classes:
(1..MAX_ACT).each do |act_id|
klass = Class.new(ActB) do
def initialize(trg)
super(trg, act_id)
end
end
Object.const_set("Act#{act_id}", klass)
end
In this case, the common base class (ActB) has a constructor with two parameters, while the child classes have a constructor with one parameter.
Running this code works well, but when I later try to instantiate one of these classes, for example
Act3.new(4)
I get the error message
NameError: undefined local variable or method `act_id' for #<Act3:0x00000006008b7990>
The error message must refer to the line
super(trg, act_id)
because this is the only place in my program where I am using this variable. However, this variable is defined a few lines above, when it says
(1..MAX_ACT).each do |act_id|
I had expected, that the do...end block creates a closure for the constructor, where act_id is bound. However, this doesn't seem to be the case.
Why does my example not work? How do I have to do it correctly?

def (and class and module) creates a fresh local scope, which doesn't inherit any locals from outside.
So you're right that the Class.new do .. end creates a closure... but the inner def doesn't share it.
If you need standard block behaviour, you can use define_method instead:
(1..MAX_ACT).each do |act_id|
klass = Class.new(ActB) do
define_method :initialize do |trg|
super(trg, act_id)
end
end
Object.const_set("Act#{act_id}", klass)
end

Just out of curiosity, there is a hack, allowing to fool scoping and still use def initialize :)
class ActB
def initialize(trg, act_id)
puts "ActID: #{act_id}"
end
end
(1..MAX_ACT).each do |act_id|
klass = Class.new(ActB) do
#act_id = act_id
def initialize(trg)
super(trg, self.class.instance_variable_get(:#act_id))
end
end
Object.const_set("Act#{act_id}", klass)
end
Act1.new :foo
#⇒ ActID: 1
Act2.new :foo
#⇒ ActID: 2

The problem here is that the block passed to Class.new is executed in the context of that class. In the context of that class, act_id is not defined. So, to fix this, you can move the method definition outside of the class initialization, like so:
(1..MAX_ACT).each do |act_id|
klass = Class.new(ActB)
klass.define_method(:initialize) do |trg|
super(trg, act_id)
end
Object.const_set("Act#{act_id}", klass)
end

Related

Definied Anonymous class in rspec won't respond to new

so I have the following anonymous class definition:
let!(:fake_class) do
Class.new(Integer) do
def initialize(value)
#value = value
end
def ==(other)
#value == other
end
def coerce(other)
[#value, other]
end
def to_s
#value.to_s
end
end
end
But when I do:
fake_class.new 4
I just get undefined method 'new' for #<Class:0x00007fc065377c88>
I've tried doing
define_method :initialize do |value|
#value = value
end
no difference
the only way it responds to new is if I do
class << self
def new(value)
#value = value
end
end
but that obviously won' work as I need it to act like a real class.
Why do I see lots of tutorials using intialize and it working as expected yet it doesn't seem to work for me? Is it becuase i'm defining it in rspec or somthing?
The issue here is nothing to do with rspec, nor anonymous classes.
The problem is that in ruby, you cannot subclass Integer*.
Ruby stores small Integers (formerly known as Fixnums) as immediate values, using some of the low bits of the word to tag it as such, instead of a pointer to an object on the heap. Because of that, you can't add methods to a single "instance" of Integer, and you can't subclass it.
If you really want an "Integer-like" class, you could construct a workaround with a class that has an integer instance variable, and forward method calls appropriately:
class FakeInteger
def initialize(integer)
#integer = integer
end
def method_missing(name, *args, &blk)
ret = #integer.send(name, *args, &blk)
ret.is_a?(Numeric) ? FakeInteger.new(ret) : ret
end
end
* Technically you can, but since you cannot instantiate any objects from it, it's pretty useless :)
Your code is correct but Integer does not respond to .new and so your child class will also not respond to .new.
irb(main):001:0> Integer.new
NoMethodError (undefined method `new' for Integer:Class)
When you call Integer(123) you actually call a global function defined here:
https://github.com/ruby/ruby/blob/v2_5_1/object.c#L3987
https://github.com/ruby/ruby/blob/v2_5_1/object.c#L3178

How to handle optional parameter for Class.new

I create a dynamic class using the Class.new method. But sometimes I call the method with a parameter to create an inherited class - sometimes without.
Option 1:
newclass = Class.new do
...
end
Option 2:
newClass = Class.new(p) do
...
end
The body of the new class is identical. But I cannot call Class.new(p) with undefined p. So I have to create an if statement and then either call Class.new with parameter or without which means I have the duplicate the code for creating the class which is not ideal since every change in my code I have to make twice. Any way how I can get around this?
You can just abstract the Class.new call and leave the block in one place. Something like this:
def create_me_a_class(superklass = Object, &block)
Class.new(superklass, &block)
end
newclass = create_me_a_class(p) do
def my_method
# whatever
end
end

Disappearing instance var

Here is my code:
class Klass
["thing", nil].each do |i|
instance_variable_set("##{i}reqs", {})
end
def initialize(var)
#reqs[var] = self
end
end
Klass.new("hello")
Which gives me the error:
in initialize': undefined method[]=' for nil:NilClass (NoMethodError)
I shouldn't be getting this error because the loop at the top should have initialized #reqs in it's second iteration. What is going on?
Instance variables belong to particular instances. That's why they are called instance variables.
In line 3, you set the instance variable called #reqs of the object Klass. In line 6, you access the instance variable called #reqs of an instance of the class Klass. Those are two completely different, distinct objects each with its own set of instance variables. Heck, those two objects don't even have the same class! (Klass's class is Class, whereas Klass.new's class is Klass.)
In line 6, #reqs is uninitialized, and uninitialized instance variables evaluate to nil.
There are many different ways to fix this, depending on your exact circumstances and requirements, the easiest way would be to initialize the instance variables in the initialize method, after all, that's what that method is there for:
class Klass
def initialize(var)
['thing', nil].each do |i|
instance_variable_set(:"##{i}reqs", {})
end
#reqs[var] = self
end
end
Klass.new('hello')
Remember, the problem was that the instance variables were initialized in one object, and accessed in another. This solution moves the initialization to the same object that was doing the reading.
However, the dual is also possible: move the reading to where the initialized variables are:
class Klass
['thing', nil].each do |i|
instance_variable_set(:"##{i}reqs", {})
end
def initialize(var)
self.class.instance_variable_get(:#reqs)[var] = self
end
end
Klass.new('hello')
This is kind of ugly, so let's add an attr_reader:
class Klass
['thing', nil].each do |i|
instance_variable_set(:"##{i}reqs", {})
end
class << self; attr_reader :reqs end
def initialize(var)
self.class.reqs[var] = self
end
end
Klass.new('hello')
Obviously, these two do very different things. It is unclear from your question which of the two you actually want.
A third possibility would be using class variables:
class Klass
['thing', nil].each do |i|
class_variable_set(:"###{i}reqs", {})
end
def initialize(var)
##reqs[var] = self
end
end
Klass.new('hello')
Note that this does yet another different thing. Again, whether you want that or not is not clear from your question.
The loop at the top is defining an instance variable for the Class, not for any object of the class.
So for the object, it doesn't exist.
From the looks of it, you want a hash common to the whole class where you store each created object in the hash. Assuming you don't have issues with class inheritance, you'd be better of with a class variable.
so...
class Klass
["thing",nil].each do |i|
class_variable_set("###{i}reqs", {})
end
def initialize(var)
##reqs[var] = self
end
end
Klass.new("hello")
If you define class like this:
class Klass
instance_variable_set(:#name, 'dog')
def self.name
#name
end
def name
#name
end
end
then
Klass.name # => 'dog'
instance = Klass.new
instance.name # => nil
Now you can see the difference. Your variable is defined on class level, not instance level.

How can I maintain a variable via closure with define_method?

I am trying to create a macro "has_accessor_for", that accepts a symbol which is used as a parameter for an internal object that it uses (the Accessorizer object). The problem I am having is, when multiple modules do the has_accessors_for, the parameter (scope) ends up being stuck on the last value it was assigned to.
I added a puts prior to the define_method, which shows that it's scope1, and then scope2... But inside the define_method, it's scope2 always. I am looking for a way to basically encapsulate that variable, so that when it the first module calls has_accessor_for, anytime my_wut is called, it will be bound to scope1... and anytime my_bleah is called, it will be bound to scope2. But as I said, right now, both my_bleah and my_wut are bound to scope2-- If I change the order of the includes in MyModel, then they will both be bound to scope1.
class Accessorizer
def initialize(record, scope)
#record = record
#scope = scope
end
def value_for(key)
#record.send key
end
end
module Magic
def has_accessors_for(scope)
accessors = {}
puts "initial: #{scope}"
define_method :get_value_for do |key|
puts "inside method #{scope}"
accessor.value_for key
end
define_method :accessor do
accessors[:scope] ||= Accessorizer.new(self, scope)
end
end
end
module SomeAccessor
extend Magic
has_accessors_for :scope1
def my_wut
get_value_for :wut
end
end
module SomeOtherAccessor
extend Magic
has_accessors_for :scope2
def my_bleah
get_value_for :bleah
end
end
class MyModel
include SomeAccessor
include SomeOtherAccessor
attr_accessor :wut, :bleah
end
m = MyModel.new
m.wut = 'wut'
m.bleah = 'bleah'
m.my_bleah
m.my_wut
output:
initial: scope1
initial: scope2
inside method scope2
inside method scope2
Short answer: the problem is not with the closures.
Long answer:
define_method :get_value_for do |key|
puts "inside method #{scope}"
accessor.value_for key
end
On a given class there can only be one method called get_value_for - the second definition will overwrite the first.
It doesn't matter so much because you're call accessor in both cases, however that method suffers from the same problem - you define it twice and so the second definition overwrites the first and you end up with only one Accessorizer object.
I think you'll need to rethink your design here.

Accessing the name of an anonymous class in superclass' self.inherited

I would like to access a class' name in its superclass MySuperclass' self.inherited method. It works fine for concrete classes as defined by class Foo < MySuperclass; end but it fails when using anonymous classes. I tend to avoid creating (class-)constants in tests; I would like it to work with anonymous classes.
Given the following code:
class MySuperclass
def self.inherited(subclass)
super
# work with subclass' name
end
end
klass = Class.new(MySuperclass) do
def self.name
'FooBar'
end
end
klass#name will still be nil when MySuperclass.inherited is called as that will be before Class.new yields to its block and defines its methods.
I understand a class gets its name when it's assigned to a constant, but is there a way to set Class#name "early" without creating a constant?
I prepared a more verbose code example with failing tests to illustrate what's expected.
Probably #yield has taken place after the ::inherited is called, I saw the similar behaviour with class definition. However, you can avoid it by using ::klass singleton method instead of ::inherited callback.
def self.klass
#klass ||= (self.name || self.to_s).gsub(/Builder\z/, '')
end
I am trying to understand the benefit of being able to refer to an anonymous class by a name you have assigned to it after it has been created. I thought I might be able to move the conversation along by providing some code that you could look at and then tell us what you'd like to do differently:
class MySuperclass
def self.inherited(subclass)
# Create a class method for the subclass
subclass.instance_eval do
def sub_class() puts "sub_class here" end
end
# Create an instance method for the subclass
subclass.class_eval do
def sub_instance() puts "sub_instance here" end
end
end
end
klass = Class.new(MySuperclass) do
def self.name=(name)
#name = Object.const_set(name, self)
end
def self.name
#name
end
end
klass.sub_class #=> "sub_class here"
klass.new.sub_instance #=> "sub_instance here"
klass.name = 'Fido' #=> "Fido"
kn = klass.name #=> Fido
kn.sub_class #=> "sub_class here"
kn.new.sub_instance #=> "sub_instance here"
klass.name = 'Woof' #=> "Woof"
kn = klass.name #=> Fido (cannot change)
There is no way in pure Ruby to set a class name without assigning it to a constant.
If you're using MRI and want to write yourself a very small C extension, it would look something like this:
VALUE
force_class_name (VALUE klass, VALUE symbol_name)
{
rb_name_class(klass, SYM2ID(symbol_name));
return klass;
}
void
Init_my_extension ()
{
rb_define_method(rb_cClass, "force_class_name", force_class_name, 1);
}
This is a very heavy approach to the problem. Even if it works it won't be guaranteed to work across various versions of ruby, since it relies on the non-API C function rb_name_class. I'm also not sure what the behavior will be once Ruby gets around to running its own class-naming hooks afterward.
The code snippet for your use case would look like this:
require 'my_extension'
class MySuperclass
def self.inherited(subclass)
super
subclass.force_class_name(:FooBar)
# work with subclass' name
end
end

Resources