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]
Related
Some background: I have an external library that uses explicit type checking instead of duck-typing in one of its methods. Something like:
def a_method(value)
case value
when Array then 'an Array'
when Hash then 'a Hash'
when Foo then 'a Foo'
end
end
Foo is defined in the library. I would like to pass another object to this method that should be treated like a Foo. Therefore, I'm subclassing Foo:
class Bar < Foo
end
which works just fine:
bar = Bar.new
a_method(bar)
#=> 'a Foo'
Unfortunately, Foo implements several methods that will break the way Bar is supposed to work, including method_missing and respond_to?. For example:
class Foo
def respond_to?(method_name)
false
end
end
Because Foo is Bar's superclass, Foo#respond_to? is invoked when calling Bar#respond_to?:
class Bar < Foo
def hello
end
end
bar = Bar.new
bar.respond_to?(:hello) #=> false
bar.method(:respond_to?) #=> #<Method: Bar(Foo)#respond_to?>
I would like to remove or bypass Foo's method in this case (i.e. from within Bar) so that:
bar.respond_to?(:hello) #=> true
bar.method(:respond_to?) #=> #<Method: Bar(Kernel)#respond_to?>
just as if Foo#respond_to? did not exist.
Any suggestions?
Suppose you had this class structure:
class A
def respond_to?(meth)
"A"
end
def cat
end
def tap
puts "tap in A"
end
end
class B < A
def respond_to?(meth)
"B"
end
end
class C < B
def respond_to?(meth)
"C"
end
end
class D < C
end
We have:
D.ancestors
#=> [D, C, B, A, Object, Kernel, BasicObject]
Further, you want:
D.new.respond_to?(:cat)
#=> true
If we write:
class D < C
def respond_to?(meth)
method(__method__).super_method.call(meth)
end
end
then:
D.new.respond_to?(:cat)
#=> C
This is not surprising, since it's the same as:
class D < C
def respond_to?(meth)
super
end
end
D.new.respond_to?(:cat)
#=> C
Now try:
class D < C
def respond_to?(meth)
method(__method__).super_method.super_method.call(meth)
end
end
then:
D.new.respond_to?(:cat)
#=> B
so we have skipped over C. Now redefine D#respond_to? as follows:
class D < C
def respond_to?(meth)
method(__method__).super_method.super_method.super_method.call(meth)
end
end
D.new.respond_to?(:cat)
#=> A
Once more:
class D < C
def respond_to?(meth)
method(__method__).super_method.super_method.super_method.super_method.call(meth)
end
end
D.new.respond_to?(:cat)
#=> True (invokes Kernel#respond_to?)
Therefore, you could do the following:
module JumpOver
def jump_over(over_mod, meth)
m = instance_method(meth)
loop do
m = m.super_method
break if m.owner > over_mod
end
define_method(meth, m)
end
end
class D
extend JumpOver
jump_over(A, :respond_to?)
jump_over(A, :tap)
end
D.methods.include?(:jump)
#=> true
D.instance_methods(false)
#=> [:respond_to?, :tap]
d = D.new
#=> #<D:0x007ff263161b58>
d.respond_to? :cat
#=> true
d.respond_to? :dog
#=> false
d.tap { |o| puts 'hi' }
# 'hi'
#=> #<D:0x007ff263161b58>
I don't understand the problem. Just implement a respond_to? in Bar and don't call super.
class Foo
def respond_to?(method)
puts 'in foo'
false
end
end
class Bar < Foo
def respond_to?(method)
puts 'in bar'
true
end
end
bar = Bar.new
bar.respond_to?(:quux) # => true
# >> in bar
This is, of course, a violation of LSP, but you specifically asked for it, so... :)
Ruby's method lookup seems to be "hard-coded". I couldn't find a way to alter it from within Ruby.
Based on Sergio Tulentsev's suggestion to re-implement the methods, however, I came up with a helper method to replace / overwrite every superclass (instance) method with its "super" method (or undefine it if there is none):
def self.revert_superclass_methods
superclass.instance_methods(false).each do |method|
super_method = instance_method(method).super_method
if super_method
puts "reverting #{self}##{method} to #{super_method}"
define_method(method, super_method)
else
puts "undefining #{self}##{method}"
undef_method(method)
end
end
end
This has basically the same effect for my purposes. Example usage:
module M
def foo ; 'M#foo' ; end
end
class A
include M
def to_s ; 'A#to_s' ; end
def foo ; 'A#foo' ; end
def bar ; 'A#bar' ; end
end
class B < A
def self.revert_superclass_methods
# ...
end
end
b = B.new
b.to_s #=> "A#to_s"
b.foo #=> "A#foo"
b.bar #=> "A#bar"
B.revert_superclass_methods
# reverting B#to_s to #<UnboundMethod: Object(Kernel)#to_s>
# reverting B#foo to #<UnboundMethod: Object(M)#foo>
# undefining B#bar
b.to_s #=> "#<B:0x007fb389987490>"
b.foo #=> "M#foo"
b.bar #=> undefined method `bar' for #<B:0x007fb389987490> (NoMethodError)
A chained method should only be called under certain circumstances in the following code.
class Klass
def foo
puts 'foo'
self
end
def bar
puts 'bar'
self
end
end
klass = Klass.new
a = 2
id = klass.foo{conditionally chain bar if a == 2}.bar
Can you insert an expression or method between chained methods that conditionally continues or halts the method chain?
This is simple and who will come after you will understand immediately:
klass = klass.foo
klass = klass.bar if a == 2
etc...
This works well if the chained methods take no arguments
klass.define_singleton_method :chain_if do |b, *s|
return unless b
klass = self
s.each do |x|
klass = klass.send x
end
klass
end
klass.foo.chain_if(true, :foo, :bar).chain_if(false, :bar)
Here some duplicated threads!
conditional chaining in ruby
Add method to an instanced object
Here I found another solution that I personally like:
my_object.tap{|o|o.method_a if a}.tap{|o|o.method_b if b}.tap{|o|o.method_c if c}
EDIT:
beware tap is defined as follows:
class Object
def tap
yield self
self
end
end
What you need might look like this, if the chained method returns a new immutable object:
class Object
def tap_and_chain
yield self
end
end
def chain_maybe(klass, condition, *args)
args[1..-1].reduce(klass.send(args.first)) { |r,m| (condition && r.send(m)) || r }
end
or:
def chain_maybe(klass, condition, *args)
first, *others = args
others = [] unless condition
others.reduce(klass.send(first)) { |r,m| r.send(m) }
end
For:
class Klass
def foo
puts 'foo'
self
end
def bar
puts 'bar'
self
end
def baz
puts 'baz'
self
end
end
c = Klass.new
chain_maybe(c, true, :foo, :bar, :baz)
foo
bar
baz
#=> #<Klass:0x007fccea8da388>
chain_maybe(c, false, :foo, :bar, :baz)
foo
#=> #<Klass:0x007fccea8da388>
chain_maybe(c, true, :foo, :bar)
foo
bar
#=> #<Klass:0x007fccea8da388>
chain_maybe(c, true, :bar, :baz)
bar
baz
#=> #<Klass:0x007fccea8da388>
If there is to be a condition for each argument, this can be generalized to:
def chain_maybe(klass, conditions, args)
return nil if conditions.empty?
first, *others = args.zip(conditions).select(&:last).map(&:first)
others.reduce(klass.send(first)) { |r,m| r.send(m) }
end
args = [:foo, :bar, :baz]
chain_maybe(c, [true, true, true], args)
foo
bar
baz
#=> #<Klass:0x007fccea8da388>
chain_maybe(c, [false, true, true], args)
bar
baz
#=> #<Klass:0x007fccea8da388>
chain_maybe(c, [true, false, true], args)
foo
baz
#=> #<Klass:0x007fccea8da388>
You can use tap if the block you are chaining is mutable, i.e: It will change the value of self (as explained here)
However, if you are chaining immutable blocks like QueryMethods, you can use try like:
data
.try { |d| group ? d.group(group) : d }
.try { |d| group ? d.order(order => :desc) : d }
try with nothing else but a block is essentially:
def try
yield self
end
(reference: #try)
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
Is there a shorter (and cleaner) way to write the following reader method:
class Foo
attr_writer :bar
def bar
return_value = #bar
self.bar = nil
return_value
end
end
Here's some output on the console to show what it does:
>> foo = Foo.new
=> #<Foo:0x1010e9cf8>
>> foo.bar
=> nil
>> foo.bar = "ok"
=> "ok"
>> foo.bar
=> "ok"
>> foo.bar
=> nil
def bar
#bar
ensure
#bar = nil
end
This can be a one-liner, if you want:
def bar
#bar ensure #bar = nil
end
def bar
#bar, rv = nil, #bar
rv
end
or
def bar
(#bar, rv = nil, #bar)[1]
end
Don't think so. Looks quite short and clean already to me!
Less clever, but perhaps more clear:
class Foo
def initialize
#bar = []
end
def bar
#bar.pop
end
def bar= (obj)
#bar[0] = obj
end
end
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