Delayed Instantiation - delayed.rb

Here's some code I wrote to delay object instantiation until an
object's methods are needed. If there's anything fooey about it, or
if there's a better version out there, please let me know :slight_smile:

--begin--

···

#########################################################################
#########################################################################
#### delayed.rb: Thunks for Object Insantiation
####
#### Author: Matthew Maycock
####
#### Email: _mr_bill_98@yahoo.com
####
#### Date: June 16th, 2004 AD!
####
####
####
#### Purpose:
####
#### Delay instantiation of objects until needed.
####
#### Overhead given by an extra indirection of dispatch
####
#### Use at your own risk!
####
####
####
#### License:
####
#### The least restrictive license applicable to this software
####
#### given that it was written in ruby and uses the ruby library.
####
#### I don't really know that much about such things, so if there
####
#### isn't anything in the way, consider this released under the
####
#### public domain, free for all!
####
#########################################################################
#########################################################################

# Delay creation of an object until some method tries to use it.
class DelayedInstance < Object
  def initialize(klass, *args)
    @object = Object.new
    ivars = Object.instance_method(:instance_variables)
    ieval = Object.instance_method(:instance_eval)
    iclss = Object.instance_method(:class)
    inspt = Object.instance_method(:inspect)

    switched = @delayed_switched = false

    switch = @delayed_switch = Proc.new {
      raise "Already switched!" if switched
      switched = @delayed_switched = true
      @object = klass.send(:new, *args)
    }

    delayed_self = self
    @object.instance_eval {
      @delayed_parent = delayed_self
      @delayed_switch = switch
      @delayed_added = false
      @delayed_eval = ieval
    }

    [:==, :===, :=~, :__id__, :__send__, :class, :clone, :display,
:dup, :eql?, :equal?, :extend, :freeze, :frozen?, :hash, :id,
     :inspect, :instance_eval, :instance_of, :instance_variable_get,
:instance_variable_set, :instance_variables, :is_a?, :kind_of?,
     :method, :methods, :new, :nil?, :object_id, :private_methods,
:protected_methods, :public_methods, :remove_instance_variable,
     :respond_to?, :send, :singleton_method_added,
:singleton_method_removed, :singleton_method_undefined,
:singleton_methods, :taint,
     :tainted?, :to_a, :to_s, :type, :untaint].each {|sym|
      eval <<-END_EVAL
        def @object.#{sym.to_s}(*args, &block)
          return super(*args, &block) unless @delayed_added
          parent = @delayed_parent
          @delayed_switch[]
          obj = @delayed_eval.bind(parent).call { @object }
          obj.send(#{sym.inspect}, *args, &block)
        end
      END_EVAL
    }

    Object.instance_method(:instance_eval).bind(@object).call
{@delayed_added = true}
  end

  def method_missing(sym, *args, &block)
    @delayed_switch[] unless @delayed_switched
    @object.send(sym, *args, &block)
  end
end

[:==, :===, :=~, :__id__, :__send__, :class, :clone, :display, :dup,
:eql?, :equal?, :extend, :freeze, :frozen?, :hash, :id,
:inspect, :instance_eval, :instance_of, :instance_variable_get,
:instance_variable_set, :instance_variables, :is_a?, :kind_of?,
:method, :methods, :new, :nil?, :object_id, :private_methods,
:protected_methods, :public_methods, :remove_instance_variable,
:respond_to?, :send, :singleton_method_added,
:singleton_method_removed, :singleton_method_undefined,
:singleton_methods, :taint,
:tainted?, :to_a, :to_s, :type, :untaint].each {|sym|
  DelayedInstance.module_eval <<-END_EVAL
    def #{sym.to_s}(*args, &block)
      @object.send(#{sym.inspect}, *args, &block)
    end
  END_EVAL
}

if __FILE__ == $0 then

  class Test
    def initialize(index)
      $stdout.puts "Something that takes a thousand hours..."
      @index = index
    end

    def display
      $stdout.puts @index
    end
  end

  data = (1..10).map {|i| DelayedInstance.new(Test, i)}
  data.each {|d| d.display}

  $stdout.puts
  foo = DelayedInstance.new(Test, 1000)
  foo.instance_eval {
    $stdout.puts "Instance Eval!"
    @santa = 100
  }

  $stdout.puts foo.instance_variables.inspect
  $stdout.puts foo.instance_eval {@santa}
end

=begin
### ruby -v: ruby 1.8.1 (2003-12-25) [i386-mswin32]
# OUTPUT
Something that takes a thousand hours...
1
Something that takes a thousand hours...
2
Something that takes a thousand hours...
3
Something that takes a thousand hours...
4
Something that takes a thousand hours...
5
Something that takes a thousand hours...
6
Something that takes a thousand hours...
7
Something that takes a thousand hours...
8
Something that takes a thousand hours...
9
Something that takes a thousand hours...
10

Something that takes a thousand hours...
Instance Eval!
["@index", "@santa"]
100
=end

[...]

#### License:
####
#### The least restrictive license applicable to this software
####
#### given that it was written in ruby and uses the ruby library.
####
#### I don't really know that much about such things, so if there
####
#### isn't anything in the way, consider this released under the
####
#### public domain, free for all!

[...]

A lot of people say "under Ruby's license" so that the library is under
exactly the same license as Ruby. This has always worried me a bit - what
Ruby's license changes in the future.

Maybe we need a "RPL 1.0" (Ruby Public License).

You can put your code in the public domain, meaning that you relinquish
copyright over it (so putting something in the public domain is not a
"license"), but someone on slashdot :wink: said that this is a problem in some
countries. (Germany?)

Alternatively, the following (MIT-style license) is very un-restrictive:
http://www.opensource.org/licenses/mit-license.php

It basically says "do anything you want, but leave me copyright notice in
the source code + NO WARRANTY".

···

In article <13383d7a.0406160752.1443e31c@posting.google.com>, matt wrote:

is it just me, or could the same results come from this:

class DelayedInstance
   def initialize(klass,*args)
     @klass = klass
     @args = args
     @object = nil
   end

   def method_missing(meth,*args,&block)
     @object = @klass.send(:new,*@args) if @object.nil?
     @object.send(meth,*args,&block)
   end
end

matt wrote:

···

Here's some code I wrote to delay object instantiation until an
object's methods are needed. If there's anything fooey about it, or
if there's a better version out there, please let me know :slight_smile:

--begin--
#########################################################################
#### delayed.rb: Thunks for Object Insantiation ####
#### Author: Matthew Maycock ####
#### Email: _mr_bill_98@yahoo.com ####
#### Date: June 16th, 2004 AD! ####
#### ####
#### Purpose: ####
#### Delay instantiation of objects until needed. ####
#### Overhead given by an extra indirection of dispatch ####
#### Use at your own risk! ####
#### ####
#### License: ####
#### The least restrictive license applicable to this software ####
#### given that it was written in ruby and uses the ruby library.
####
#### I don't really know that much about such things, so if there
####
#### isn't anything in the way, consider this released under the ####
#### public domain, free for all! ####
#########################################################################

# Delay creation of an object until some method tries to use it.
class DelayedInstance < Object
  def initialize(klass, *args)
    @object = Object.new
    ivars = Object.instance_method(:instance_variables)
    ieval = Object.instance_method(:instance_eval)
    iclss = Object.instance_method(:class)
    inspt = Object.instance_method(:inspect)

    switched = @delayed_switched = false

    switch = @delayed_switch = Proc.new {
      raise "Already switched!" if switched
      switched = @delayed_switched = true
      @object = klass.send(:new, *args)
    }

    delayed_self = self
    @object.instance_eval {
      @delayed_parent = delayed_self
      @delayed_switch = switch
      @delayed_added = false
      @delayed_eval = ieval
    }

    [:==, :===, :=~, :__id__, :__send__, :class, :clone, :display,
:dup, :eql?, :equal?, :extend, :freeze, :frozen?, :hash, :id,
     :inspect, :instance_eval, :instance_of, :instance_variable_get,
:instance_variable_set, :instance_variables, :is_a?, :kind_of?,
     :method, :methods, :new, :nil?, :object_id, :private_methods,
:protected_methods, :public_methods, :remove_instance_variable,
     :respond_to?, :send, :singleton_method_added,
:singleton_method_removed, :singleton_method_undefined,
:singleton_methods, :taint,
     :tainted?, :to_a, :to_s, :type, :untaint].each {|sym|
      eval <<-END_EVAL
        def @object.#{sym.to_s}(*args, &block)
          return super(*args, &block) unless @delayed_added
          parent = @delayed_parent
          @delayed_switch
          obj = @delayed_eval.bind(parent).call { @object }
          obj.send(#{sym.inspect}, *args, &block)
        end
      END_EVAL
    }

    Object.instance_method(:instance_eval).bind(@object).call
{@delayed_added = true}
  end

  def method_missing(sym, *args, &block)
    @delayed_switch unless @delayed_switched
    @object.send(sym, *args, &block)
  end
end

[:==, :===, :=~, :__id__, :__send__, :class, :clone, :display, :dup,
:eql?, :equal?, :extend, :freeze, :frozen?, :hash, :id,
:inspect, :instance_eval, :instance_of, :instance_variable_get,
:instance_variable_set, :instance_variables, :is_a?, :kind_of?,
:method, :methods, :new, :nil?, :object_id, :private_methods,
:protected_methods, :public_methods, :remove_instance_variable,
:respond_to?, :send, :singleton_method_added,
:singleton_method_removed, :singleton_method_undefined,
:singleton_methods, :taint,
:tainted?, :to_a, :to_s, :type, :untaint].each {|sym|
  DelayedInstance.module_eval <<-END_EVAL
    def #{sym.to_s}(*args, &block)
      @object.send(#{sym.inspect}, *args, &block)
    end
  END_EVAL
}

if __FILE__ == $0 then

  class Test
    def initialize(index)
      $stdout.puts "Something that takes a thousand hours..."
      @index = index
    end

    def display
      $stdout.puts @index
    end
  end

  data = (1..10).map {|i| DelayedInstance.new(Test, i)}
  data.each {|d| d.display}

  $stdout.puts
  foo = DelayedInstance.new(Test, 1000)
  foo.instance_eval {
    $stdout.puts "Instance Eval!"
    @santa = 100
  }

  $stdout.puts foo.instance_variables.inspect
  $stdout.puts foo.instance_eval {@santa}
end

=begin
### ruby -v: ruby 1.8.1 (2003-12-25) [i386-mswin32]
# OUTPUT
Something that takes a thousand hours...
1
Something that takes a thousand hours...
2
Something that takes a thousand hours...
3
Something that takes a thousand hours...
4
Something that takes a thousand hours...
5
Something that takes a thousand hours...
6
Something that takes a thousand hours...
7
Something that takes a thousand hours...
8
Something that takes a thousand hours...
9
Something that takes a thousand hours...
10

Something that takes a thousand hours...
Instance Eval!
["@index", "@santa"]
100
=end

It's just you :slight_smile:

···

---------------------------------------------
matt@linux1 matt $ ruby -v
ruby 1.8.1 (2004-04-24) [i686-linux-gnu]
matt@linux1 matt $ cat moo.rb

class DelayedInstance
  def initialize(klass, *args, &block)
    @klass = klass
    @args = args
    @block = block
    @object = nil
  end

  def method_missing(sym, *args, &block)
    STDOUT.puts "Inside method missing: #{sym}"
    @object = @klass.send(:new, *@args, &@block) if @object.nil?
    @object.send(sym, *args, &block)
  end
end

class Test
  def initialize(*args, &block)
    STDOUT.puts "Initializing test: #{args.inspect}"
    @data = args
  end

  def show_data
    STDOUT.puts @data.inspect
  end

  def to_s
    "You don't own space, so stop acting like you do."
  end
end

test = DelayedInstance.new(Test, 1, 2, 3, 4)

p "to_s"
p test.to_s
puts

p "show_data"
p test.show_data

matt@linux1 matt $ ruby moo.rb
"to_s"
"#<DelayedInstance:0x4028e14c>"

"show_data"
Inside method missing: show_data
Initializing test: [1, 2, 3, 4]
[1, 2, 3, 4]
nil
---------------------------------------------

Anders Borch <ruby@deck.dk> wrote in message news:<40D17236.8020600@deck.dk>...

is it just me, or could the same results come from this:

class DelayedInstance
   def initialize(klass,*args)
     @klass = klass
     @args = args
     @object = nil
   end

   def method_missing(meth,*args,&block)
     @object = @klass.send(:new,*@args) if @object.nil?
     @object.send(meth,*args,&block)
   end
end

That just means that you need something like the BlankSlate class,
(http://jimweirich.umlcoop.net/index.cgi/Tech/Ruby/BlankSlate.rdoc)
which doesn't inherit all of the default methods from Object. That
way, even built-in methods pass through to the delayed object.

There's also a hack I remember seeing out there somewhere that allows
one object to "become" another, so the DelayedInstance would actually
adopt the identity of the instantiated object, removing the additional
indirection on each method call.

Of course, an argument could be made that it's good to have some
indication that you're dealing with a proxy object, and let some
methods from that object stay exposed.

The bigger problem with this simple example is that it breaks the
"duck typing" idiom; using 'method_missing' indirection (esp. if
built-in methods are removed, as I suggest above) will give misleading
results if you call 'responds_to?'. You should be able to fix it by
overriding 'responds_to?' in the DelayedInstance class, and checking
the methods of your @object, instead of the proxy.

Lennon

Lennon Day-Reynolds wrote:

There's also a hack I remember seeing out there somewhere that allows
one object to "become" another, so the DelayedInstance would actually
adopt the identity of the instantiated object, removing the additional
indirection on each method call.

Are you referring to the EvilRuby-project? (See http://evil.rubyforge.org/\)

Regards,
Florian Gross

That's not the original source for the hack(s) I was talking about,
but it seems to cover pretty much all of the cases I mentioned.

Metaprogramming is fun!

Lennon

···

On Fri, 18 Jun 2004 02:43:27 +0900, Florian Gross <flgr@ccan.de> wrote:

Lennon Day-Reynolds wrote:

> There's also a hack I remember seeing out there somewhere that allows
> one object to "become" another, so the DelayedInstance would actually
> adopt the identity of the instantiated object, removing the additional
> indirection on each method call.

Are you referring to the EvilRuby-project? (See http://evil.rubyforge.org/\)

Regards,
Florian Gross

Lennon Day-Reynolds <rcoder@gmail.com> wrote in message news:<5d4c6124040617110925ad6885@mail.gmail.com>...

That's not the original source for the hack(s) I was talking about,
but it seems to cover pretty much all of the cases I mentioned.

Maybe you meant the become smalltalk stuff people were chatting about?

The current version of my code is at
http://www.cs.drexel.edu/~ummaycoc/delayed.rb

Anyway, I'm still having some problems with this code, and I can't
seem to fix it up to work. There are two issues:

  1) a binding to a method isn't being kept for some reason. I talk
about @singleton_added. To see that it's not working, please refer to
http://www.cs.drexel.edu/~ummaycoc/delayed.rb.debug - which has some
debug print statements. After redefining singleton_method_added - the
call to super should invoke the magic, I think. Also, unless I call
super, a singleton-method singleton_method_added doesn't call
singleton_method_added - which I thought
was a bit counter intuitive, is it supposed to be like that?

  2) how to bind a method object to an unrelated class (ie not a
subclass). I'm thinking of hacking the source so that one can go from
method foo -> module containing method foo. then you could add any
object's method to another object. I just ask whether if anyone knows
off the bat whether there is a technical issue in the way.
     
Below shows that point 1 should be plausible...
Thanks for any and all help!

···

------------------------------------------
matt@linux1 matt $ ruby -v
ruby 1.8.1 (2004-04-24) [i686-linux-gnu]
matt@linux1 matt $ cat test.rb

class Test
  def initialize
    @method_before = nil
    @method_after = nil
  end

  def meow
    STDOUT.puts "In before."
    return unless @method_before.nil?

    @method_before = self.method(:meow)
    def self.meow
      STDOUT.puts "In after."
    end
    @method_after = self.method(:meow)
  end

  def before
    @method_before.call
  end

  def after
    @method_after.call
  end
end

x = Test.new

before = x.method(:meow)
x.meow
after = x.method(:meow)

STDOUT.puts "CALLING!"
before.call
x.before

STDOUT.puts
after.call
x.after

STDOUT.puts
STDOUT.puts "UNDEF"
class << x
  self
end.class_eval {
  undef_method :meow
}

STDOUT.puts "CALLING!"
before.call
x.before

STDOUT.puts
after.call
x.after

matt@linux1 matt $ ruby test.rb
In before.
CALLING!
In before.
In before.

In after.
In after.

UNDEF
CALLING!
In before.
In before.

In after.
In after.

------------------------------------------

Maybe you meant the become smalltalk stuff people were chatting about?

[...]

Anyway, I'm still having some problems with this code, and I can't
seem to fix it up to work. There are two issues:

[...]

  2) how to bind a method object to an unrelated class (ie not a
subclass). I'm thinking of hacking the source so that one can go from
method foo -> module containing method foo. then you could add any
object's method to another object. I just ask whether if anyone knows
off the bat whether there is a technical issue in the way.

You can take a look at evil.rb, which contains an example of how to
create a proxy class and manipulate the m_tbl.

You can also implement these 'lazy objects' using Object#become; in many
cases, for RObject objects, it is safe. The implementation in evil.rb
does many safety checks (circular class chains, EX_IVARS, method cache
invalidation, etc) missing in the other ones you might find around.

···

On Sun, Jun 20, 2004 at 04:33:19AM +0900, matt wrote:

--
Running Debian GNU/Linux Sid (unstable)
batsman dot geo at yahoo dot com

Apples have meant trouble since eden.
  -- MaDsen Wikholm, mwikholm@at8.abo.fi