Lazy evaluation in Ruby
Asked Answered
K

3

22

I have a situation for Ruby, where an object is possibly necessary to be created, but it is not sure. And as the creation of the object might be costly I am not too eager creating it. I think this is a clear case for lazy loading. How can I define an object which is not created only when someone sends a message to it? The object would be created in a block. Is there a way for simple lazy loading/initialisation in Ruby? Are these things supported by some gems, which provide different solutions for various cases of lazy initialisation of objects? Thanks for your suggestions!

Khmer answered 17/3, 2010 at 10:26 Comment(0)
L
41

There are two ways.

The first is to let the caller handle lazy object creation. This is the simplest solution, and it is a very common pattern in Ruby code.

class ExpensiveObject
  def initialize
    # Expensive stuff here.
  end
end

class Caller
  def some_method
    my_object.do_something
  end

  def my_object
    # Expensive object is created when my_object is called. Subsequent calls
    # will return the same object.
    @my_object ||= ExpensiveObject.new
  end
end

The second option is to let the object initialise itself lazily. We create a delegate object around our actual object to achieve this. This approach is a little more tricky and not recommended unless you have existing calling code that you can't modify, for example.

class ExpensiveObject        # Delegate
  class RealExpensiveObject  # Actual object
    def initialize
      # Expensive stuff here.
    end

    # More methods...
  end

  def initialize(*args)
    @init_args = args
  end

  def method_missing(method, *args)
    # Delegate to expensive object. __object method will create the expensive
    # object if necessary.
    __object__.send(method, *args)
  end

  def __object__
    @object ||= RealExpensiveObject.new(*@init_args)
  end
end

# This will only create the wrapper object (cheap).
obj = ExpensiveObject.new

# Only when the first message is sent will the internal object be initialised.
obj.do_something

You could also use the stdlib delegate to build this on top of.

Lumberjack answered 17/3, 2010 at 10:51 Comment(3)
In the first example I need to keep instance of Caller class. Right? But what is the difference for me - to keep Caller class instance or to keep Expensive class instance?Dalessio
In the first example, the Caller class is just an example of how you would use the ExpensiveObject class. The difference: introduce laziness where you use the ExpensiveObject (simple), or introduce laziness in the ExpensiveObject itself (slightly more complicated).Lumberjack
@molf: Whenever you override method_missing you must also override respond_to? (or preferably respond_to_missing? in 1.9.2). See blog.marc-andre.ca/2010/11/methodmissing-politely.htmlSimulant
G
7

If you want to lazily evaluate pieces of code, use a proxy:

class LazyProxy

  # blank slate... (use BasicObject in Ruby 1.9)
  instance_methods.each do |method| 
    undef_method(method) unless method =~ /^__/
  end

  def initialize(&lazy_proxy_block)
    @lazy_proxy_block = lazy_proxy_block
  end

  def method_missing(method, *args, &block)
    @lazy_proxy_obj ||= @lazy_proxy_block.call # evaluate the real receiver
    @lazy_proxy_obj.send(method, *args, &block) # delegate unknown methods to the real receiver
  end
end

You then use it like this:

expensive_object = LazyProxy.new { ExpensiveObject.new }
expensive_object.do_something

You can use this code to do arbitrarily complex initialization of expensive stuff:

expensive_object = LazyProxy.new do
  expensive_helper = ExpensiveHelper.new
  do_really_expensive_stuff_with(expensive_helper)
  ExpensiveObject.new(:using => expensive_helper)
end
expensive_object.do_something

How does it work? You instantiate a LazyProxy object that holds instructions on how to build some expensive object in a Proc. If you then call some method on the proxy object, it first instantiates the expensive object and then delegates the method call to it.

Gazelle answered 17/3, 2010 at 13:50 Comment(0)
W
0

With Ruby 3.x, I use the gem concurrent-ruby.
A possible lazy initialization use case looks as following:

require 'concurrent'

# put expensive code inside a "future":
very_lazy = Concurrent::Promises.future { some_expensive_code_block }
# the "future" starts performing work in background

# use
puts very_lazy.value   # blocks, until the "future" is ready
puts very_lazy.value   # repeated calls just re-use existing value

If I'm wrong, someone please correct me.

Waltraudwaltz answered 8/8, 2023 at 21:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.