3
\$\begingroup\$

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?

\$\endgroup\$
  • \$\begingroup\$ 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 \$\endgroup\$ – m_x Dec 20 '13 at 9:31
  • 1
    \$\begingroup\$ @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). \$\endgroup\$ – Chris Jester-Young Jan 5 '14 at 20:11
  • \$\begingroup\$ oh, cool. learned something today :) \$\endgroup\$ – m_x Jan 6 '14 at 11:23
2
\$\begingroup\$

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.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.