Class variables leaking when defined from a module? - ruby

Consider that I have a module XYZ. When included in a class, it extends the base class and adds a class variable ##foo inside it. It also extends the class with the methods to do some get and set.
module XYZ
def self.included(base)
base.send :extend, ClassMethods
base.class_eval do
##foo ||= []
end
end
module ClassMethods
def foo
class_variable_get("##foo")
end
def foo=(arg)
class_variable_set("##foo", arg)
end
def dynamic_set(*args)
foo += args # This doesn't work
end
def dynamic_set_2(*args)
class_variable_set("##foo", class_variable_get("##foo") + args)
end
end
end
Now, consider the usage:
class A
include XYZ
end
A.foo #=> []
A.class_variable_get("##foo") #=> []
A.dynamic_set 1, 2 #=> NoMethodError: undefined method `+' for nil:NilClass
A.dynamic_set_2 1, 2 #=> [1,2]
A.foo #=> [1,2]
A.class_variable_get("##foo") #=> [1,2]
The snippet makes sense and gets the work done, but I'm not able to figure out why A.dynamic_set 1, 2 didn't work.
Coming to the main part of the question - If I define a new class B as:
class B
include XYZ
end
B.foo #=> [1,2] => Why? How did B.foo get these values?
B.class_variable_get("##foo") #=> [1,2] => Why?
B.dynamic_set_2 3, 4
B.foo #=> [1,2,3,4]
A.foo #=> [1,2,3,4]
Why are B and A sharing the same class variable when ##foo is defined on the class level (with class_eval)?
I understand about implications of using class variable and class instance variables. Just trying to figure out why this doesn't work as intended, to clear some concepts :)

I'm not able to figure out why A.dynamic_set 1, 2 didn't work.
Use an explicit receiver when calling setters:
def dynamic_set(*args)
foo += args # This doesn't work
self.foo += args # This DOES work
end
Coming to the main part of the question
TL;DR: Don’t use class variables. Use instance variables at class level.
module XYZ
def self.included(base)
base.send :extend, ClassMethods
base.class_eval do
#foo ||= []
end
end
module ClassMethods
def foo
instance_variable_get("#foo")
end
def foo=(arg)
instance_variable_set("#foo", arg)
end
def dynamic_set(*args)
self.foo += args # This doesn't work
end
def dynamic_set_2(*args)
class_variable_set("#foo", instance_variable_get("#foo") + args)
end
end
end
It’s worth to mention it in the answer.
module XYZ
def self.included(base)
base.class_eval { ##foo ||= [] }
end
end
The code above pollutes the XYZ class variables with ##foo because ##foo is hoisted in XYZ module.
base.class_variable_set(:##foo, []) instead would not pollute XYZ.

Related

How is it I can't access a module function from within a class from the same module in ruby?

I'm having some issues with the access rules of ruby. At one point I thought that something like this worked:
module Foo
def bar(x)
puts "#{x}"
end
class Baz
def initialize(x)
bar(x)
end
end
end
Foo::Baz.new(3)
but somehow it no longer does. I've tried to declare bar using self.bar and Foo.bar, but neither work. What am I missing?
The inclusion of the module methods isn't "automatic", I think because it can lead to unexpected behavior.
You can try making them a class method as you mentioned, or including the module and getting access to the methods there:
module Foo
class << self
def bar(x)
puts "#{s}" % x
end
def s
"s1"
end
end
def bar(x)
puts "#{s}" % x
end
def s
"s2"
end
class Baz
include Foo
def initialize(x)
bar(x)
Foo.bar(x)
end
end
end
Foo::Baz.new(3)
# s2
# s1

How to work through name collisions in ruby

Two modules Foo and Baa respectively define a method with the same name name, and I did include Foo and include Baa in a particular context.
When I call name, how can I disambiguate whether to call the name method of Foo or Baa?
Only the order of modules inclusion decides which one will get called. Can't have both with the same name - the latter will override the former.
Of course, you can do any tricks, just from the top of my head:
module A
def foo
:foo_from_A
end
end
module B
def foo
:foo_from_B
end
end
class C
def initialize(from)
#from = from
end
def foo
from.instance_method(__method__).bind(self).call
end
private
attr_reader :from
end
C.new(A).foo #=> :a_from_A
C.new(B).foo #=> :a_from_B
But that's no good for real life use cases :)
Technically, there is no name collision because the method foo is redefined.
In the following exemple, A.foo is redefined and is never called
module A
def foo
raise "I'm never called"
end
end
module B
def foo
puts :foo_from_B
end
end
class C
include A
include B
end
C.new.foo
# =>
# foo_from_B
If you write A and B module, you can use super to call previous definition of foo. As if it where an inherited method.
module A
def foo
puts :foo_from_A
end
end
module B
def foo
super
puts :foo_from_B
end
end
class C
include A
include B
end
C.new.foo
# =>
# foo_from_A
# foo_from_B
There are side effects and I would not use this but this is doing the trick :
module A
def foo
puts :foo_from_A
end
end
module B
def foo
puts :foo_from_B
end
end
class C
def self.include_with_suffix(m, suffix)
m.instance_methods.each do |method_name|
define_method("#{method_name}#{suffix}", m.instance_method(method_name))
end
end
include_with_suffix A, "_from_A"
include_with_suffix B, "_from_B"
end
c= C.new
c.foo_from_A
c.foo_from_B
begin
c.foo
rescue NoMethodError
puts "foo is not defined"
end
# =>
# foo_from_A
# foo_from_B
# foo is not defined
Provided none of the methods of Foo or Baa call name (which seems a reasonable assumption), one can simply create aliases.
module Foo
def name; "Foo#name"; end
end
module Baa
def name; "Baa#name"; end
end
class C
include Foo
alias :foo_name :name
include Baa
alias :baa_name :name
undef_method :name
end
c = C.new
c.foo_name
#=> "Foo#name"
c.baa_name
#=> "Baa#name"
C.instance_methods & [:foo_name, :baa_name, :name]
#=> [:foo_name, :baa_name]
The keyword alias is documented here. One may alternatively use the method #alias_method. See this blog for a comparison of the two.
Module#undef_method is not strictly necessary. It's just to ensure that an exception is raised if name is called.
You should definetely read about method lookups.
Anyway, I would do it this way:
module Foo
def name
:foo
end
end
module Bar
def name
:bar
end
end
class MyClass
include Foo
include Bar
def foo_name
Foo.instance_method(:name).bind(self).call
end
def bar_name
Bar.instance_method(:name).bind(self).call
end
#
# or even like this: obj.name(Foo)
#
def name(mod)
mod.instance_method(:name).bind(self).call
end
end
BTW if you are using Module#instance_method and UnboundMethod#bind you don't really need to include specific module. This code works:
Foo.instance_method(:name).bind('any object (e.g. string)').call

Avoid class pollution while including module

Is there a concise way to limit method visibility within the module while including it? In other words, I'd like to limit polluting the class with helper methods only used in the included module.
module Bar
def call
hide_me
end
private
# make this method only callable within this module
def hide_me
'visible'
end
end
class Foo
include Bar
def unrelated_method
hide_me
end
end
# that's ok
Foo.new.call #=> 'visible'
# that's not
Foo.new.unrelated_method #=> 'visible'
I'm ok with calling it via Bar.instance_method(:hide_me).bind(self).call, I just don't want to worry about accessing or redefining a helper method from some module.
You can wrap a class into the module and use private methods within the class, like so:
module Bar
def call
BarClass.new.call
end
class BarClass
def call
hide_me
end
private
def hide_me
puts "invisible"
end
end
end
class Foo
include Bar
def call_bar
call
end
def this_method_fails
hide_me
end
end
One can do what you want by including the module to the class and then undefining the unwanted included methods.
First construct a module.
module M
def cat
puts "meow"
end
def dog
puts "woof"
end
def owl
puts "who?"
end
private
def frog
puts "ribbit"
end
def pig
puts "oink"
end
end
Confirm the methods now exist.
M.instance_methods(false)
#=> [:cat, :dog, :owl]
M.private_instance_methods(false)
#=> [:frog, :pig]
Create the class, including the module M.
class C
def froggie
frog
end
def cat
puts "meow-meow"
end
include M
end
Check the instance methods.
C.instance_methods & [:cat, :dog, :owl, :froggie]
#=> [:cat, :froggie, :dog, :owl]
C.private_instance_methods & [:frog, :pig]
#=> [:frog, :pig]
and confirm :cat is owned by C and not by M.
C.instance_method(:cat).owner
#=> C
Now use the method Module#undef_method to undefine the unwanted methods from the module.
class C
[:cat, :owl, :pig].each { |m|
undef_method(m) unless instance_method(m).owner == self }
end
The unless... clause is needed so that the instance method :cat defined in C is not undefined.
Confirm the methods were undefined.
C.instance_methods & [[:cat, :dog, :owl, :froggie]
#=> [:cat, :froggie, :dog]
C.private_instance_methods & [:frog, :pig]
#=> [:frog]
Execute the methods.
c = C.new
c.cat
#=> "meow-meow"
c.dog
#=> "woof"
c.froggie
#=> "ribbit"

Issue with creating DSL syntax

I have the following code:
module A
def self.included(base)
base.extend(ClassMethods)
end
def foo
a = bar
puts a
end
def bar(str="qwe")
str
end
module ClassMethods
end
end
class B
include A
def bar(str="rty")
str
end
end
B.new.foo #=> "rty"
I wish class B to look like this:
class B
include A
bar "rty"
end
B.new.foo #=> rty
or
class B
include A
HelperOptions.bar "rty" # HelperOptions class should be in module A
end
B.new.foo #=> rty
I tried using define_method, class_eval, and initialize. How can be implemented syntax bar 'rty' or HelperOptions.bar 'rty' and what shall be done in module A?
If I got your question properly you want to define a class method A.bar that defines an instance method B#bar that returns it's argument, you can do it like this:
module A
def self.included(base)
base.extend(ClassMethods)
end
def foo
puts bar
end
module ClassMethods
def bar(str)
define_method(:bar) { str }
end
end
end
class B
include A
bar 'rty'
end
B.new.bar
# => "rty"
B.new.foo
# Output: rty

Ruby methods similar to attr_reader

I'm trying to make a method similar to attr_reader but I can't seem to get the instance of the class that the method gets called in.
class Module
def modifiable_reader(*symbols)
# Right here is where it returns Klass instead of #<Klass:0x1df25e0 #readable="this">
mod = self
variables = symbols.collect { |sym| ("#" << sym.to_s).to_sym }
attr_reader *symbols
(class << ModifyMethods; self; end).instance_eval do
define_method(*symbols) do
mod.instance_variable_get(*variables)
end
end
end
end
class Object
module ModifyMethods; end
def modify(&block)
ModifyMethods.instance_eval(&block)
end
end
class Klass
modifiable_reader :readable
def initialize
#readable = "this"
end
end
my_klass = Klass.new
my_klass.modify do
puts "Readable: " << readable.to_s
end
I'm not sure what it is you're trying to do.
If it helps, the spell for attr_reader is something like this:
#!/usr/bin/ruby1.8
module Kernel
def my_attr_reader(symbol)
eval <<-EOS
def #{symbol}
##{symbol}
end
EOS
end
end
class Foo
my_attr_reader :foo
def initialize
#foo = 'foo'
end
end
p Foo.new.foo # => "foo"
What I can understand from your code is that you want to have the modify block to respond to the instance methods of Klass, that's as simple as:
class Klass
attr_reader :modifiable
alias_method :modify, :instance_eval
def initialize(m)
#modifiable = m
end
end
Klass.new('john').modify do
puts 'Readable %s' % modifiable
end
About this tidbit of code:
def modifiable_reader(*symbols)
# Right here is where it returns Klass instead of #<Klass:0x1df25e0 #readable="this">
mod = self
...
Probably this can give you a hint of what is going on:
Class.superclass # => Module
Klass.instance_of?(Class) # => true
Klass = Class.new do
def hello
'hello'
end
end
Klass.new.hello # => 'hello'
When you are adding methods to the Module class, you are also adding methods to the Class class, which will add an instance method to instances of Class (in this case your class Klass), at the end this means you are adding class methods on your Klass class

Resources