Watching the discussion on the Ruby Rogues Parley about the difference between procs and lambdas I realized that, by being tightly coupled with the context they are created, procs could easily create memory leaks.
The following example illustrates that:
class Printer
@messages = []
def self.print
puts yield
end
def self.lazy_print(&block)
@messages << block
end
end
class Foo
def initialize(i)
Printer.print { "hey #{i}" }
end
end
class LeakyFoo
def initialize(i)
Printer.lazy_print { "hey #{i}" }
end
end
100.times { |i| Foo.new i }
100.times { |i| LeakyFoo.new i }
GC.start
puts "Foo count = #{ObjectSpace.each_object(Foo).count}" #=> Foo count = 0
puts "LeakyFoo count = #{ObjectSpace.each_object(LeakyFoo).count}" #=> LeakyFoo count = 100
As you can see, because the Printer class caches every block for future use, not one LeakyFoo has been collected by the GC.
Unfortunately is extremely easy to forget about this behavior, so anyone (even me) is likely to write the following piece of code to define an instance finalizer:
class FatFoo
def initialize(i)
ObjectSpace.define_finalizer( self, proc { puts "finalizing #{i}"} )
end
end
100.times { |i| FatFoo.new i }
GC.start
puts "FatFoo count = #{ObjectSpace.each_object(FatFoo).count}" #=> 100
One solution to this problem is to create the Proc object in a different context:
class SlimFoo
def initialize(i)
ObjectSpace.define_finalizer( self, self.class.finalizer(i) )
end
def self.finalizer(i)
proc { puts "finalizing #{i}" }
end
end
100.times { |i| SlimFoo.new i }
GC.start
puts "SlimFoo count = #{ObjectSpace.each_object(SlimFoo).count}"
Sadly, this is the kind of behavior that can bring subtle and catastrophic bugs.