46

I have a a hash

foo = {'bar'=>'baz'}

I would like to call foo.bar #=> 'baz'

My motivation is rewriting an activerecord query into a raw sql query (using Model#find_by_sql). This returns a hash with the SELECT clause values as keys. However, my existing code relies on object.method dot notation. I'd like to do minimal code rewrite. Thanks.

Edit: it appears Lua has this feature:

point = { x = 10, y = 20 }   -- Create new table
print(point["x"])            -- Prints 10
print(point.x)               -- Has exactly the same meaning as line above
grace
  • 59
  • 6
user94154
  • 16,176
  • 20
  • 77
  • 116

5 Answers5

81
>> require 'ostruct'
=> []
>> foo = {'bar'=>'baz'}
=> {"bar"=>"baz"}
>> foo_obj = OpenStruct.new foo
=> #<OpenStruct bar="baz">
>> foo_obj.bar
=> "baz"
>>
Hooopo
  • 1,380
  • 10
  • 16
  • 4
    Use of OpenStruct is discouraged since each instantiation invalidates the Ruby method cache, which slows down your entire application. – Chris Heald Jun 14 '15 at 22:12
  • @ChrisHeald: care to elaborate a bit on this proposition? Who discourages the use of OpenStruct? Does it invalidate the *entire* method cache or just the method cache for the symbol? – Steen Jul 16 '15 at 08:31
  • 4
    @Steen If you google "ruby method cache" the first couple of hits will be explanations of it by Charlie Somerville and the late James Golick. Before Ruby 2.1, instantiating an ostruct method dumped the *entire* method cache. In 2.1+, it dumps the method cache for all ostruct instances and instances of its subclasses. – Chris Heald Jul 16 '15 at 15:57
  • 1
    This is not recursive ! Prefer @gabor solution – Thomas Decaux Feb 11 '17 at 15:24
35

What you're looking for is called OpenStruct. It's part of the standard library.

Avdi
  • 18,340
  • 6
  • 53
  • 62
22

A good solution:

class Hash
  def method_missing(method, *opts)
    m = method.to_s
    if self.has_key?(m)
      return self[m]
    elsif self.has_key?(m.to_sym)
      return self[m.to_sym]
    end
    super
  end
end

Note: this implementation has only one known bug:

x = { 'test' => 'aValue', :test => 'bar'}
x.test # => 'aValue'

If you prefer symbol lookups rather than string lookups, then swap the two 'if' condition

Gabor Garami
  • 1,245
  • 8
  • 26
  • Might not have solved his problem, but this solved one of mine. Took a lot of searching to find this, but I thank you! – gregf Nov 03 '10 at 17:30
  • 3
    I've been doing this `self[m] || self[m.to_s] || super` instead of the `if/else` block – Kyle Sep 19 '12 at 05:26
  • 1
    That only works if the hash key doesn't match an existing method name (like Hash#zip, for example). – Joe Van Dyk Jan 18 '13 at 19:54
  • @JoeVanDyk The same limitation applies to OpenStruct. If you had a key-value pair in the hash used to initialize an OpenStruct object, and that key had the same name as an existing method of OpenStruct, it would prefer the method call to returning the hash value. The only difference I see is that OS is standard, and *maybe* a little faster, and has some other utilities for object management. Using the method_missing way is equally as powerful, and you can extend existing code/libs easily. – tralston Nov 24 '13 at 21:37
  • OpenStruct is *really* slow. http://stackoverflow.com/questions/1177594/ruby-struct-vs-openstruct/5440064#5440064 Plus it clears the method cache. – Joe Van Dyk Nov 24 '13 at 22:29
  • @Kyle - even though your 1-liner is elegant, it will fail when the value of a key is false. I suggest not using that, otherwise you will have to come back to debug it. Suggest using the method suggested by Gabor – lsu_guy Jul 04 '16 at 00:06
6

Rather than copy all the stuff out of the hash, you can just add some behaviour to Hash to do lookups.

If you add this defintion, you extend Hash to handle all unknown methods as hash lookups:

class Hash
  def method_missing(n)
    self[n.to_s]
  end
end

Bear in mind that this means that you won't ever see errors if you call the wrong method on hash - you'll just get whatever the corresponding hash lookup would return.

You can vastly reduce the debugging problems this can cause by only putting the method onto a specific hash - or as many hashes as you need:

a={'foo'=>5, 'goo'=>6}
def a.method_missing(n)
   self[n.to_s]
end

The other observation is that when method_missing gets called by the system, it gives you a Symbol argument. My code converted it into a String. If your hash keys aren't strings this code will never return those values - if you key by symbols instead of strings, simply substitute n for n.to_s above.

Dafydd Rees
  • 6,941
  • 3
  • 39
  • 48
  • 13
    Someone did this early on in a large project I worked on. It caused all kinds of subtle bugs because incorrect method calls on any hash in the system would just return nil instead of alerting us with a NoMethodError. It took forever to remove from the system because code all over the system had come to depend on it. It's one of the most disastrous code-class extensions I've ever seen. – Avdi Nov 18 '09 at 03:49
  • 4
    To (mostly) fix that problem, you could do `if has_key? n.to_s then self[n.to_s] else raise NoMethodError` – jtbandes Nov 18 '09 at 03:54
  • Avdi - I'd certainly agree that abuse/overuse of metaprogramming is a problem. There's probably a better way to solve the overall problem (OpenStruct probably) but I just got it working as an exercise. – Dafydd Rees Nov 18 '09 at 04:04
6

There are a few gems for this. There's my recent gem, hash_dot, and a few other gems with similar names I discovered as I released mine on RubyGems, including dot_hash.

HashDot allows dot notation syntax, while still addressing concerns about NoMethodErrors addressed by @avdi. It is faster, and more traversable than an object created with OpenStruct.

require 'hash_dot'
a = {b: {c: {d: 1}}}.to_dot
a.b.c.d => 1

require 'open_struct'
os = OpenStruct.new(a)
os.b => {c: {d: 1}}
os.b.c.d => NoMethodError

It also maintains expected behavior when non-methods are called.

a.non_method => NoMethodError

Please feel free to submit improvements or bugs to HashDot.

steel
  • 11,883
  • 7
  • 72
  • 109
  • This looks really great. Is there any downsides to use `hash_dot` or a place where that may be documented on the Github page? I see performance is a bit of one. But, one thing that I can think of is, what happens if the key has the same name as an existing method name on a `Hash`. Does an error get raised or anything? etc. – Joshua Pinter Jul 25 '23 at 17:00
  • 1
    @JoshuaPinter no error is raised in that case, but that could be a good, optional setting to introduce. Feel free to open a pull request! – steel Aug 16 '23 at 14:08
  • Thanks for the response. If we end up using this I definitely will. I think that's a great safety feature to have in there to avoid silent collisions. – Joshua Pinter Aug 16 '23 at 14:55