# Ruby 1.9 interner

I'm currently implementing an interner in the style of Guava's Interner, where:

1. an intern pool is maintained for an immutable class
2. whenever a new instance of that class is created, it's checked to see if it's equal to an existing instance in the intern pool
3. if such an existing instance is found, it's returned, otherwise the new instance is added to the pool and returned

Of course, in Ruby, you can replace methods like new and [], so my implementation does just that:

module Interner
def self.extended(obj)
intern_pool = Hash.new do |hash, key|
# Ideally, this should be a deep freeze, but it's not supported
# (see http://bugs.ruby-lang.org/issues/show/2509).
if key.respond_to?(:clone)
key = key.clone
key.freeze if key.respond_to?(:freeze)
end
hash[key] = key
end
[:new, :[]].each do |name|
if obj.respond_to?(name)
obj.singleton_class.module_exec(obj.method(name)) do |orig|
define_method name do |*args, &block|
intern_pool[orig[*args, &block]]
end
end
end
end
end
end


Example usage:

Foo = Struct.new :foo, :bar do
extend Interner
end

a = Foo[1, 2]
b = Foo[3, 4]
c = Foo.new(1, 2)
d = Foo.new(3, 4)
a.equal?(c)  # => true
b.equal?(d)  # => true


I realise that my implementation is less than perfect. What improvements can you recommend?

• nice little lib :) Maybe you could make it thread-safe, by synchronizing access to intern_pool with a mutex ? also I think the same functionnality could be achieved using a gem like memoist and memoizing ::new on the extended class – m_x Dec 20 '13 at 9:31
• @m_x It's actually different from memoist. Memoist caches values based on the arguments. An interner caches the resulting values themselves. So if two calls (with different arguments) result in equal objects, they should return the same instance. See my example with Foo[1, 2] vs Foo.new(1, 2). – Chris Jester-Young Jan 5 '14 at 20:11
• oh, cool. learned something today :) – m_x Jan 6 '14 at 11:23

I can't see much I'd change. It's a pretty neat concept, well done. I wouldn't change much. First, the minor things.

I would consider changing the name of the argument to Interner.extended from obj to klass.

Being a member of the "lots of tiny methods" fan club, I'd consider this:

  def self.extended(klass)
intern_pool = make_intern_pool
...
end

def self.make_intern_pool
Hash.new do |hash, key|
key = freeze_key(key)
hash[key] = key
end
end

def self.freeze_key(key)
# Ideally, this should be a deep freeze, but it's not supported
# (see http://bugs.ruby-lang.org/issues/show/2509).
if key.respond_to?(:clone)
key = key.clone
key.freeze if key.respond_to?(:freeze)
end
key
end


Now, on to bigger things. Interner makes a resonable assumption that new is a factory method. However, its assumption that [] is a factory method is less reasonable. That's certainly the case for Struct, and perhaps for many other classes, but not necessarily always. I would prefer to see a way to make explicit the name(s) of the factory methods, so that I have control over which methods are wrapped by Interner. There should still be an easy, default way to use Interner that wraps new, but perhaps there could be another way to use it that lets you specify which methods to wrap.