Delegation question, where I want prototype style delegation

I want to make an object that behaves like another object would, if that
object had had it's #each method redefined.

I can do this with extend(), but that permanently damages the object.

I can do it with delegate/method_missing, except I need to reimplement
every method of the delegatee, which sucks.

I could do this in prototype based languages, where you can effectively
create a new object that behaves like another object with a few differences,
but don't see a way in an OO language like ruby.

Below is an example of what I want to do, implemented with extend.

But it has a problem, it modifies the target object, but I may want
to create mutiple delegates to the
target, each with a different set of "views", that behave like the
target object would if it
had its #each method redefined.

Btw, what I'm actually doing is I have a calendar object, and I want
to create various views
of the calendar, ones including events but not todos, ones that appear
to only have
components that occur in a particular period, etc.

class Base
  include Enumerable

  def initialize(ary)
    @ary = ary.to_a
  end

  def each
    @ary.each{|a| yield a}
  end

  def show
    inject("show: ") {|accum, o| accum + o.to_s + ","}
  end
end

module Negate
  def each(&block)
    super do |a|
      yield -a
    end
  end
end

module Add10
  def each(&block)
    super do |a|
      yield a+10
    end
  end
end

o = Base.new(1..3)
o.extend Negate.dup
o.extend Add10
o.extend Negate.dup

o.each {|_| puts _ }

puts o.show

What you really want in this case is normal inheritance, so the overridden each() method replaces the original.

Then the only thick becomes getting the existing object into an equivalent subclass form. We can use a little Ruby magic to track subclasses and do the conversion for us:

class Base
   def self.subclasses
     @subclasses ||=
   end

   def self.inherited(subclass)
     subclasses << subclass
   end

   def self.subclass(snake_case_name)
     camel_case_name = snake_case_name.gsub(/(?:\A|_)(.)/) { $1.capitalize }
     subclasses.find { |sc| sc.to_s == camel_case_name }
   end

   include Enumerable

   def initialize(ary)
     @ary = Array(ary)
   end

   def each(&block)
     @ary.each(&block)
   end

   def show
     "show: #{to_a.join(", ")}"
   end

   def method_missing(method, *args, &block)
     if method.to_s =~ /\Aas_(\w+)\z/ and (sc = self.class.subclass($1))
       sc.new(@ary)
     else
       super
     end
   end
end

class Negated < Base
   def each
     super { |e| yield -e }
   end
end

class Plus10 < Base
   def each
     super { |e| yield e + 10 }
   end
end

puts "Base:"
b = Base.new(1..3)
puts b.show

puts "Negated:"
puts b.as_negated.show

puts "Plus 10:"
puts b.as_plus_10.show

__END__

Hope that helps.

James Edward Gray II

···

On May 6, 2008, at 1:00 AM, Sam Roberts wrote:

I want to make an object that behaves like another object would, if that
object had had it's #each method redefined.

you can play with this:

cfp:~ > cat a.rb
class Base
   instance_methods.each do |m|
     unless m[%r/^__/]
       old = m
       new = "__#{ old }__"
       new << '?' if new.sub!('?', '')
       new << '!' if new.sub!('!', '')
       alias_method new, old
       undef_method old
     end
   end

   def initialize object
     @object = object
   end

   def method_missing m, *a, &b
     @object.__send__ m, *a, &b
   end

   Delegates = {}

   def / m
     Delegates[m] ||=
       __dup__.__instance_eval__ do
         extend m
         self
       end
   end
end

module Reverse
   def each *a, &b
     reverse.each *a, &b
   end
end

a = Base.new [2,4]

p a.each{}

p (a/Reverse).each{}

cfp:~ > ruby a.rb
[2, 4]
[4, 2]

a @ http://codeforpeople.com/

···

On May 6, 2008, at 12:00 AM, Sam Roberts wrote:

But it has a problem, it modifies the target object, but I may want
to create mutiple delegates to the
target, each with a different set of "views", that behave like the
target object would if it
had its #each method redefined.

--
we can deny everything, except that we have the possibility of being better. simply reflect on that.
h.h. the 14th dalai lama

Educational code, but doesn't quite do the trick, because the views
aren't stackable.

In my example I negated, added 10, and negated again. With yours:

puts "Stacked operations:"
puts b.as_negated.as_plus_10.as_negated.show

=> in `method_missing': undefined method `as_plus_10' for
#<Negated:0x261ec @ary=[1, 2, 3]> (NoMethodError)

The end result of this would have to be an instance of

  class Negated < Plus10 < Negated < Base
  end

or something...

Also, my real class has much more complex internal state and
relationships, it isn't just
a wrapper for an Array, and creating a new instance isn't desireable.
I'm trying to create
calendar views, where the view of the calendar looks like the real
calendar (including
reflecting changes in the base calendar), but iterates over only dates
in a particular range,
or only journal entries.

Thanks,
Sam

···

On Tue, May 6, 2008 at 5:58 AM, James Gray <james@grayproductions.net> wrote:

On May 6, 2008, at 1:00 AM, Sam Roberts wrote:

> I want to make an object that behaves like another object would, if that
> object had had it's #each method redefined.
>

What you really want in this case is normal inheritance, so the overridden
each() method replaces the original.

Ara, thanks for the suggestion. It suffers from the same
non-stackability problem:

p ((a/Reverse)/Reverse).each{}
=> [4,2]

However, the combination of this and James' suggestion made me think
maybe I should be cloning my src object, and then modifying it.
clone() will get the singleton class, but won't do a deep copy, so it
will still share most of the internal state of the original class. I
think this will work for me:

Classes defined as in my original, but do the extending like:

o = Base.new(1..3)
n = o.clone.extend Negate.dup
an = n.clone.extend Add.dup
nan = an.clone.extend Negate.dup

puts o.show
puts n.show
puts an.show
puts nan.show

% ruby m.rb
show: 1,2,3,
show: -1,-2,-3,
show: 9,8,7,
show: -9,-8,-7,

···

On Tue, May 6, 2008 at 7:25 AM, ara.t.howard <ara.t.howard@gmail.com> wrote: