Brit200313 Brit200313 - 3 months ago 19
Ruby Question

OpenStruct issue with Ruby 2.3.1

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.

Answer

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.

Comments