aetherknight/recursive-open-struct

Is recursive-open-struct really necessary?

Closed this issue · 3 comments

All respect where due, I used this gem for the last year (thank you), then worked out I really didn't need it in my circumstances.

We can get JSON (included in Ruby standard library http://www.ruby-doc.org/stdlib-2.1.5/libdoc/json/rdoc/index.html, so no additional dependencies) to do the heavy lifting for us and instruct it to coerce nested attributes into OpenStructs. It will also remain respectful of arrays.

require 'json'
require 'ostruct'

hash = {:a=>1,:b=>[{:c=>2,:d=>[{:e=>3,:f=>4},{:e=>5,:f=>6}]},{:c=>4,:d=>[{:e=>7,:f=>8},{:e=>9,:f=>10}]},{:c=>6,:d=>[{:e=>11,:f=>12},{:e=>13,:f=>14}]}]}

json = hash.to_json
# => "{\"a\":1,\"b\":[{\"c\":2,\"d\":[{\"e\":3,\"f\":4},{\"e\":5,\"f\":6}]},{\"c\":4,\"d\":[{\"e\":7,\"f\":8},{\"e\":9,\"f\":10}]},{\"c\":6,\"d\":[{\"e\":11,\"f\":12},{\"e\":13,\"f\":14}]}]}"

object = JSON.parse(json, object_class: OpenStruct)
# => #<OpenStruct a=1, b=[#<OpenStruct c=2, d=[#<OpenStruct e=3, f=4>, #<OpenStruct e=5, f=6>]>, #<OpenStruct c=4, d=[#<OpenStruct e=7, f=8>, #<OpenStruct e=9, f=10>]>, #<OpenStruct c=6, d=[#<OpenStruct e=11, f=12>, #<OpenStruct e=13, f=14>]>]>

object.a
# => 1

object.b
# => [#<OpenStruct c=2, d=[#<OpenStruct e=3, f=4>, #<OpenStruct e=5, f=6>]>, #<OpenStruct c=4, d=[#<OpenStruct e=7, f=8>, #<OpenStruct e=9, f=10>]>, #<OpenStruct c=6, d=[#<OpenStruct e=11, f=12>, #<OpenStruct e=13, f=14>]>]

object.b.class
# => Array

object.b.size
# => 3

object.b[0].class
# => OpenStruct

object.b[0].c
# => 2

object.b[0].d
# => [#<OpenStruct e=3, f=4>, #<OpenStruct e=5, f=6>]

object.b[0].d.class
# => Array

object.b[0].d.size
# => 2

object.b[0].d[1]
# => #<OpenStruct e=5, f=6>

object.b[0].d[1].f
# => 6

Is this doing everything that this gem does, or am I missing something?

I originally wrote ROS about 5 years ago when I was initially teaching myself Ruby, and enough other people find it useful that I continue to maintain it.

There are probably some significant differences in performance between using the strategy you use above (turning a hash to JSON, parsing it, then eagerly creating all of the nested objects) and using ROS (no parsing, opportunistically creates sub-ROSes), but this is probably not a big deal if this is done once for a configuration file that is not huge.

One feature ROS has, although I'm certain it has some unresolved bugs (see issue #9 ), is the ability to set values and then return a hash with the new values.

That said, there are other similar libraries/gems out there that provide similar functionality, although I haven't closely investigated how well they handle nested hashes or the array case. Eg, Hashie::Mash and Rash.

Hey thanks for your feedback @aetherknight, I can see the differences now and understand where your coming from.

I wrote a quick benchmark (and it took actually writing this to see and understand the differences). The performance gap is significant, ROS being twice as fast. But to be fair, nested hashes are not being created as (Recursive)OpenStruct objects so the comparison isn't really equal. If ROS had a :recurse_over_hashes option, then the results might be closer. You can see when accessing the nested attribute I had to use hash keys. Here's my dirty little benchmark:

require 'benchmark'

Benchmark.bm do |r|
  h = {:a=>1,:b=>[{:c=>2,:d=>[{:e=>3,:f=>4},{:e=>5,:f=>6}]},{:c=>4,:d=>[{:e=>7,:f=>8},{:e=>9,:f=>10}]},{:c=>6,:d=>[{:e=>11,:f=>12},{:e=>13,:f=>14}]}]}
  N = 10000

  r.report("JSON") do
    require 'json'
    require 'ostruct'
    N.times { 
      o = JSON.parse(h.to_json, :object_class => OpenStruct) 
      o.b[0].d[1].f
    }
  end

  r.report("ROS ") do
    require 'recursive-open-struct'
    N.times { 
      o = RecursiveOpenStruct.new(h, :recurse_over_arrays => true) 
      o.b[0][:d][1][:f]
    }
  end
end

I run the iteration 10,000 times to get a decent time. The result on my system is as follows:

method user system total real
JSON 1.960000 0.040000 2.000000 2.036688
ROS 0.980000 0.010000 0.990000 0.983654
o = RecursiveOpenStruct.new(h, :recurse_over_arrays => true) 
o.b[0][:d][1][:f]

Hmm, that's odd. What version of recursive-open-struct are you testing? With version 0.5.0, I am able to do:

o = RecursiveOpenStruct.new(h, :recurse_over_arrays => true)
o.b[0].d[1].f

without issue.

I just created a third benchmark that calls the keys using method calls and got the following results:

method user system total real
JSON 1.360000 0.010000 1.370000 1.376528
ROS 0.720000 0.000000 0.720000 0.720925
ROS method names 1.080000 0.010000 1.090000 1.089541

So still about 20% faster than the JSON approach, as compared to the 50% faster of only mostly doing hash lookups. Again though, for most use-cases (eg, configuration) I don't think the performance impact will be very noticeable.

One observation on ROS: it opportunistically creates nested ROS's only when necessary. However, when it recurses over an array, it currently turns every hash in the array into an ROS as well. The sample hash being used for benchmarking loses a lot of its advantage due to the arrays of hashes.