I have a Builder class that lets you add to one of it's instance variables:
class Builder
def initialize
#lines = []
end
def lines
block_given? ? yield(self) : #lines
end
def add_line( text )
#lines << text
end
end
Now, how do I change this
my_builder = Builder.new
my_builder.lines { |b|
b.add_line "foo"
b.add_line "bar"
}
p my_builder.lines # => ["foo", "bar"]
Into this?
my_builder = Builder.new
my_builder.lines {
add_line "foo"
add_line "bar"
}
p my_builder.lines # => ["foo", "bar"]
class Builder
def initialize
#lines = []
end
def lines(&block)
block_given? ? instance_eval(&block) : #lines
end
def add_line( text )
#lines << text
end
end
my_builder = Builder.new
my_builder.lines {
add_line "foo"
add_line "bar"
}
p my_builder.lines # => ["foo", "bar"]
You can also use the method use in ruby best practice using the length of arguments with arity:
class Foo
attr_accessor :list
def initialize
#list=[]
end
def bar(&blk)
blk.arity>0 ? blk.call(self) : instance_eval(&blk)
end
end
x=Foo.new
x.bar do
list << 1
list << 2
list << 3
end
x.bar do |foo|
foo.list << 4
foo.list << 5
foo.list << 6
end
puts x.list.inspect
Related
Hash checks its keys with eql?:
foo = 'str'
bar = 'str'
foo.equal?(bar) #=> false
foo.eql?(bar) #=> true
h = { foo => 1 }
h[foo] #=> 1
h[bar] #=> 1
But this doesn't work if I use my own class as a key:
class Cl
attr_reader :a
def initialize(a)
#a = a
end
def eql?(obj)
#a == obj.a
end
end
foo = Cl.new(10)
bar = Cl.new(10)
foo.equal?(bar) #=> false
foo.eql?(bar) #=> true
h = { foo => 1 }
h[foo] #=> 1
h[bar] #=> nil
Why does the last line return nil instead of 1?
eql? must be used in conjunction with a hash method that returns a hash code:
class Cl
attr_reader :a
def initialize(a)
#a = a
end
def eql?(obj)
#a == obj.a
end
def hash
#a
end
end
See this blog post.
How could I improve this code so there is no duplicate code and it would allow me to add new similar methods dynamically?
def fabric_ids=(property_name)
#fabric_ids = [] if #fabric_ids.blank?
#fabric_ids << $property_values[property_name]
end
def work_ids=(property_name)
#work_ids = [] if #work_ids.blank?
#work_ids << $property_values[property_name]
end
def type_ids=(property_name)
#type_ids = [] if #type_ids.blank?
#type_ids << $property_values[property_name]
end
You can define your methods dynamically, using Module#define_method, like this:
%w(fabric_ids work_ids type_ids).each do |name|
define_method("#{name}=") do |property_name|
instance_variable_set(name, []) if instance_variable_get(name).blank?
instance_variable_get(name) << $property_values[property_name]
end
end
You might want to consider organizing this as hash. Here's one way to do that.
Code
ITEMS = [:fabric, :work, :kind]
class MyClass
attr_accessor :ids
def initialize
#ids = Hash.new { |h,k| h[k] = [] }
end
ITEMS.each do |item|
define_method("#{item.to_s}_properties") { properties_by_key(item) }
define_method("add_#{item.to_s}_properties") { |*property_names|
add_properties_by_key(item, *property_names) }
end
private
def add_properties_by_key(key, *property_names)
property_names.each { |name| self.ids[key] << $property_values[name] }
end
def properties_by_key(key)
self.ids[key]
end
end
p MyClass.instance_methods(false)
# [:ids, :ids=,
# :fabric_properties, :add_fabric_properties,
# :work_properties, :add_work_properties,
# :kind_properties, :add_kind_properties]
Example
$property_values = { color: "blue", weight: "heavy", cost: "average" }
my_class = MyClass.new
my_class.add_fabric_properties(:color, :weight, :cost)
my_class.add_work_properties(:weight, :cost)
my_class.add_kind_properties(:color)
p my_class.fabric_properties #=> ["blue", "heavy", "average"]
p my_class.work_properties #=> ["heavy", "average"]
p my_class.kind_properties #=> ["blue"]
my_class.add_kind_properties(:cost)
p my_class.kind_properties #=> ["blue", "average"]
There seem to be a mistake in my code. However I just can't find it out.
class Class
def attr_accessor_with_history(attr_name)
attr_name = attr_name.to_s
attr_reader attr_name
attr_writer attr_name
attr_reader attr_name + "_history"
class_eval %Q{
##{attr_name}_history=[1,2,3]
}
end
end
class Foo
attr_accessor_with_history :bar
end
f = Foo.new
f.bar = 1
f.bar = 2
puts f.bar_history.to_s
I would expect it to return an array [1,2,3]. However, it doesn't return anything.
You shouldn't be opening Class to add new methods. That's what modules are for.
module History
def attr_accessor_with_history(attr_name)
attr_name = attr_name.to_s
attr_accessor attr_name
class_eval %Q{
def #{attr_name}_history
[1, 2, 3]
end
}
end
end
class Foo
extend History
attr_accessor_with_history :bar
end
f = Foo.new
f.bar = 1
f.bar = 2
puts f.bar_history.inspect
# [1, 2, 3]
And here's the code you probably meant to write (judging from the names).
module History
def attr_accessor_with_history(attr_name)
attr_name = attr_name.to_s
class_eval %Q{
def #{attr_name}
##{attr_name}
end
def #{attr_name}= val
##{attr_name}_history ||= []
##{attr_name}_history << #{attr_name}
##{attr_name} = val
end
def #{attr_name}_history
##{attr_name}_history
end
}
end
end
class Foo
extend History
attr_accessor_with_history :bar
end
f = Foo.new
f.bar = 1
f.bar = 2
puts f.bar_history.inspect
# [nil, 1]
Solution:
class Class
def attr_accessor_with_history(attr_name)
ivar = "##{attr_name}"
history_meth = "#{attr_name}_history"
history_ivar = "##{history_meth}"
define_method(attr_name) { instance_variable_get ivar }
define_method "#{attr_name}=" do |value|
instance_variable_set ivar, value
instance_variable_set history_ivar, send(history_meth) << value
end
define_method history_meth do
value = instance_variable_get(history_ivar) || []
value.dup
end
end
end
Tests:
describe 'Class#attr_accessor_with_history' do
let(:klass) { Class.new { attr_accessor_with_history :bar } }
let(:instance) { instance = klass.new }
it 'acs as attr_accessor' do
instance.bar.should be_nil
instance.bar = 1
instance.bar.should == 1
instance.bar = 2
instance.bar.should == 2
end
it 'remembers history of setting' do
instance.bar_history.should == []
instance.bar = 1
instance.bar_history.should == [1]
instance.bar = 2
instance.bar_history.should == [1, 2]
end
it 'is not affected by mutating the history array' do
instance.bar_history << 1
instance.bar_history.should == []
instance.bar = 1
instance.bar_history << 2
instance.bar_history.should == [1]
end
end
You will find a solution for your problem in Sergios answer. Here an explanation, what's going wrong in your code.
With
class_eval %Q{
##{attr_name}_history=[1,2,3]
}
you execute
#bar_history = [1,2,3]
You execute this on class level, not in object level.
The variable #bar_history is not available in a Foo-object, but in the Foo-class.
With
puts f.bar_history.to_s
you access the -never on object level defined- attribute #bar_history.
When you define a reader on class level, you have access to your variable:
class << Foo
attr_reader :bar_history
end
p Foo.bar_history #-> [1, 2, 3]
#Sergio Tulentsev's answer works, but it promotes a problematic practice of using string eval which is in general fraught with security risks and other surprises when the inputs aren't what you expect. For example, what happens to Sergio's version if one calls (no don't try it):
attr_accessor_with_history %q{foo; end; system "rm -rf /"; def foo}
It is often possible to do ruby meta-programming more carefully without string eval. In this case, using simple interpolation and define_method of closures with instance_variable_[get|set], and send:
module History
def attr_accessor_with_history(attr_name)
getter_sym = :"#{attr_name}"
setter_sym = :"#{attr_name}="
history_sym = :"#{attr_name}_history"
iv_sym = :"##{attr_name}"
iv_hist = :"##{attr_name}_history"
define_method getter_sym do
instance_variable_get(iv_sym)
end
define_method setter_sym do |val|
instance_variable_set( iv_hist, [] ) unless send(history_sym)
send(history_sym).send( :'<<', send(getter_sym) )
instance_variable_set( iv_sym, val #)
end
define_method history_sym do
instance_variable_get(iv_hist)
end
end
end
Here is what should be done. The attr_writer need be defined withing class_eval instead in Class.
class Class
def attr_accessor_with_history(attr_name)
attr_name = attr_name.to_s
attr_reader attr_name
#attr_writer attr_name ## moved into class_eval
attr_reader attr_name + "_history"
class_eval %Q{
def #{attr_name}=(value)
##{attr_name}_history=[1,2,3]
end
}
end
end
I wrote the following code:
class Actions
def initialize
#people = []
#commands = {
"ADD" => ->(name){#people << name },
"REMOVE" => ->(n=0){ puts "Goodbye" },
"OTHER" => ->(n=0){puts "Do Nothing" }
}
end
def run_command(cmd,*param)
#commands[cmd].call param if #commands.key?(cmd)
end
def people
#people
end
end
act = Actions.new
act.run_command('ADD','joe')
act.run_command('ADD','jack')
puts act.people
This works, however, when the #commands hash is a class variable, the code inside the hash doesn't know the #people array.
How can I make the #commands hash be a class variable and still be able to access the specific object instance variables?
You could use instance_exec to supply the appropriate context for the lambdas when you call them, look for the comments to see the changes:
class Actions
# Move the lambdas to a class variable, a COMMANDS constant
# would work just as well and might be more appropriate.
##commands = {
"ADD" => ->(name) { #people << name },
"REMOVE" => ->(n = 0) { puts "Goodbye" },
"OTHER" => ->(n = 0) { puts "Do Nothing" }
}
def initialize
#people = [ ]
end
def run_command(cmd, *param)
# Use instance_exec and blockify the lambdas with '&'
# to call them in the context of 'self'. Change the
# ##commands to COMMANDS if you prefer to use a constant
# for this stuff.
instance_exec(param, &##commands[cmd]) if ##commands.key?(cmd)
end
def people
#people
end
end
EDIT Following #VictorMoroz's and #mu's recommendations:
class Actions
def initialize
#people = []
end
def cmd_add(name)
#people << name
end
def cmd_remove
puts "Goodbye"
end
def cmd_other
puts "Do Nothing"
end
def people
p #people
end
def run_command(cmd, *param)
cmd = 'cmd_' + cmd.to_s.downcase
send(cmd, *param) if respond_to?(cmd)
end
end
act = Actions.new
act.run_command('add', 'joe')
act.run_command(:ADD, 'jill')
act.run_command('ADD', 'jack')
act.run_command('people') # does nothing
act.people
Or
class Actions
ALLOWED_METHODS = %w( add remove other )
def initialize
#people = []
end
def add(name)
#people << name
end
def remove
puts "Goodbye"
end
def other
puts "Do Nothing"
end
def people
p #people
end
def run_command(cmd, *param)
cmd = cmd.to_s.downcase
send(cmd, *param) if ALLOWED_METHODS.include?(cmd)
end
end
act = Actions.new
act.run_command('add', 'joe')
act.run_command(:add, 'jill')
act.run_command('add', 'jack')
act.run_command('people') # does nothing
act.people
class Foo
def initialize
#bar = []
end
def changed_callback
puts "Bar has been changed!"
end
def bar
#bar
end
def bar=(a)
#bar = a
self.changed_callback() # (hence why this doesn't just use attr_accessor)
end
def bar<<(a)
#bar.push(a)
self.changed_callback()
end
end
f = Foo.new()
f.bar = [1,2,3]
=> "Bar has been changed!"
f.bar << 4
=> "Bar has been changed!"
puts f.bar.inspect
=> [1,2,3,4]
Is anything like that possible?
Thanks!
You need to somehow extend the object returned by Foo#bar with an appropriate #<< method. Something like this, maybe?
class Foo
module ArrayProxy
def <<(other)
#__foo__.changed_callback
super
end
end
def initialize
#bar = []
end
def changed_callback
puts 'Bar has been changed!'
end
def bar
return #bar if #bar.is_a?(ArrayProxy)
#bar.tap {|bar| bar.extend(ArrayProxy).instance_variable_set(:#__foo__, self) }
end
def bar=(a)
#bar = a
changed_callback # (hence why this doesn't just use attr_accessor)
end
end
f = Foo.new
f.bar = [1,2,3]
# "Bar has been changed!"
f.bar << 4
# "Bar has been changed!"
puts f.bar.inspect
# => [1,2,3,4]