LazyLoad

E

Erik Veenstra

Imagine, you're building a CVS like repository. A repository
has modules, a module has branches and a branch has _a lot of_
items (history!). This repository is handled by a standalone
server with the FTP protocol as "frontend".

In a naive implementation, you simply load all items (except
the contents of the items) in memory, so they are readily
available. In pure OO modeling theories, this is usually true:
"All objects live in core.".

In reality, you don't want to do that. You only want to load
all items of a branch if they are referred to (and optionally
unload it after a while). So we introduce Branch@loaded and
move the invocation of Branch#load from Branch#initialize to
each place in the code where Branch@items is used. *Each*
place, don't forget even one place! Sooner or later, you'll
forget one! It's too tricky... And bad coding...

So I came up with this LazyLoad, a generic lazy-loading class.
We can initialize Branch@items to LazyLoad.new(self, :load,
:items) instead of Hash.new. Whenever this object is referred
to (e.g. with @items.keys), LazyLoad#method_missing is invoked.
This method invokes Branch#load, gets the object Branch@items
(which now refers to a filled Hash) and sends the original
message to this Branch@items. This instance of LazyLoad now
dies in peace.

I implemented LazyLoad (see below) and use it in a real
situation. Seems to work. The server starts really fast and the
user thinks that all branches are loaded.

I embedded the backend in the commandline tool as well. If you
use this commandline tool to synchronize the local workset with
the repository, you usually want to load only *one* branch, not
all of them. The speed benefit is huge, whereas the impact on
the code is close to zero!

The code below demonstrates this theory: Step 1 is the naive
implementation of Branch, step2 is the enhanced implementation
of Branch and step3 implements LazyLoad itself. (Steps 1 and 2
are just examples of the use of LazyLoad. They are not
complete.)

Comments? Ideas? Something I overlooked?

gegroet,
Erik V. - http://www.erikveen.dds.nl/

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

# STEP 1, NAIVE IMPLEMENTATION

class Branch
def initialize
@items = {}

load
end

def load
@items = {}

# Fill @items... EXPENSIVE, TIME CONSUMING, MEMORY HUNGRY!
end
end

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

# STEP 2, INTRODUCING LAZYLOAD

class Branch
def initialize
@items = LazyLoad.new(self, :load, :items)
end

def load
@items = {}

# Fill @items... EXPENSIVE, TIME CONSUMING, MEMORY HUNGRY!
end
end

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

# STEP 3, IMPLEMENTATION OF LAZYLOAD

class LazyLoad
def initialize(object, load_method, property)
@object = object
@property = property
@load_method = load_method
end

def method_missing(method_name, *parms, &block)
@object.send(@load_method)
@object.instance_eval("@#{@property.to_s}").send(method_name,
*parms, &block)
end
end

----------------------------------------------------------------
 
D

Dave Burt

Erik said:
...
Comments? Ideas? Something I overlooked?
...
class LazyLoad
def initialize(object, load_method, property)
@object = object
@property = property
@load_method = load_method
end

def method_missing(method_name, *parms, &block)
@object.send(@load_method)
@object.instance_eval("@#{@property.to_s}").send(method_name,
*parms, &block)
end
end

Interesting. Here's a potential problem. You won't hit method_missing for
methods that Object has. In particular, a client might use dup, ==, to_s,
class, kind_of?, and so on. You might try getting around some of this by
using Delegate from the standard library, or Facets' BlankSlate IIRC.

I was thinking three parameters was too many for LazyLoad#initialize (just
provide a block which loads and returns the loaded value), and started to
rewrite it, when I remembered MenTaLguY's lazy.rb, which does something very
similar.

STEP 4, INTRODUCING lazy.rb

class Branch
def initialize
@items = promise do
@items = {}
# Fill @items... EXPENSIVE, TIME CONSUMING, MEMORY HUNGRY!
@items
end
end
end

Cheers,
Dave
 
E

Erik Veenstra

Interesting. Here's a potential problem. You won't hit
method_missing for methods that Object has. In particular, a
client might use dup, ==, to_s, class, kind_of?, and so on.
You might try getting around some of this by using Delegate
from the standard library, or Facets' BlankSlate IIRC.

I was aware of this problem and was already working it out. My
solution is to overwrite all (except a few) already defined
methods, so they call method_missing.
I was thinking three parameters was too many for
LazyLoad#initialize (just provide a block which loads and
returns the loaded value), and started to rewrite it, when I
remembered MenTaLguY's lazy.rb, which does something very
similar.

Right... Maybe using a block was just to obvious...

New versions below.

(I keep the method Branch#load, because it not only fills
Branch@items, but Branch@snapshots as well.)

Thanks.

More comments? More ideas? More things I overlooked?

gegroet,
Erik V. - http://www.erikveen.dds.nl/

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

class LazyLoad
instance_methods.each do |method_name|
unless ["__send__", "__id__"].include?(method_name)
class_eval <<-"EOF"
def #{method_name}(*parms, &block)
method_missing:)#{method_name}, *parms, &block)
end
EOF
end
end

def initialize(&block)
@block = block
end

def method_missing(method_name, *parms, &block)
@block.call.send(method_name, *parms, &block)
end
end

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

class Branch
def initialize
@items = LazyLoad.new{load; @items}
@snapshots = LazyLoad.new{load; @snapshots}
end

def load
@items = {}
@snapshots = {}

# Fill @items and @snapshots...
# EXPENSIVE, TIME CONSUMING, MEMORY HUNGRY!
end
end

----------------------------------------------------------------
 
E

Erik Veenstra

As a side effect of the last improvement, the attr_reader now
works too.

gegroet,
Erik V. - http://www.erikveen.dds.nl/

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

require "lazyload"

class Thing
attr_reader :prop1
attr_reader :prop2
attr_writer :prop2

def initialize
@prop1 = LazyLoad.new{:it_works}
@prop2 = LazyLoad.new{:nothing}
end
end

thing = Thing.new

thing.prop2 = :this_too

p thing.prop1
p thing.prop2

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

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,995
Messages
2,570,236
Members
46,825
Latest member
VernonQuy6

Latest Threads

Top