2

In Ruby 2.1.5 and 2.2.4, creating a new Collector returns the correct result.

require 'ostruct'
module ResourceResponses
  class Collector < OpenStruct
    def initialize
      super
      @table = Hash.new {|h,k| h[k] = Response.new }
    end
  end

  class Response
    attr_reader :publish_formats, :publish_block, :blocks, :block_order
    def initialize
      @publish_formats = []
      @blocks = {}
      @block_order = []
    end
  end  
end

 > Collector.new
 => #<ResourceResponses::Collector>
 Collector.new.responses
 => #<ResourceResponses::Response:0x007fb3f409ae98 @block_order=[], @blocks=  {}, @publish_formats=[]>

When I upgrade to Ruby 2.3.1, it starts returning back nil instead.

> Collector.new
=> #<ResourceResponses::Collector>
> Collector.new.responses
=> nil

I've done a lot of reading around how OpenStruct is now 10x faster in 2.3 but I'm not seeing what change was made that would break the relationship between Collector and Response. Any help is very appreciated. Rails is at version 4.2.7.1.

Luka Kerr
  • 4,161
  • 7
  • 39
  • 50
Brit200313
  • 728
  • 5
  • 20

1 Answers1

7

Let's have a look at the implementation of method_missing in the current implementation:

def method_missing(mid, *args) # :nodoc:
  len = args.length
  if mname = mid[/.*(?==\z)/m]
    if len != 1
      raise ArgumentError, "wrong number of arguments (#{len} for 1)", caller(1)
    end
    modifiable?[new_ostruct_member!(mname)] = args[0]
  elsif len == 0
    if @table.key?(mid)
      new_ostruct_member!(mid) unless frozen?
      @table[mid]
    end
  else
    err = NoMethodError.new "undefined method `#{mid}' for #{self}", mid, args
    err.set_backtrace caller(1)
    raise err
  end
end

The interesting part is the block in the middle that runs when the method name didn't end with an = and when there are no addition arguments:

if @table.key?(mid)
  new_ostruct_member!(mid) unless frozen?
  @table[mid]
end

As you can see the implementation first checks if the key exists, before actually reading the value.

This breaks your implementation with the hash that returns a new Response.new when a key/value is not set. Because just calling key? doesn't trigger the setting of the default value:

hash = Hash.new { |h,k| h[k] = :bar }
hash.has_key?(:foo)
#=> false
hash
#=> {}
hash[:foo]
#=> :bar
hash
#=> { :foo => :bar }

Ruby 2.2 didn't have this optimization. It just returned @table[mid] without checking @table.key? first.

spickermann
  • 100,941
  • 9
  • 101
  • 131