kristopolous
kristopolous

Reputation: 1848

JS Style Object Referencing in Ruby

Given how expressive Ruby is, I'm wondering if anyone has ever tried to create a class or module that will mimick JS object syntax. For instance, in JS I can of course do this:

[1]  var obj = {a: 'b'};
[2]  obj.c = 'd';
[3]  obj.a = 123
[4]  obj['e'] = 'f';
[5]  obj.e = obj['a']

In Ruby I can, as of now, have code that will permit me to do something like this:

[1]  obj = {'a' => 'b'}.to_js
[2]  # obj.c = 'd' << This is what I can't solve, 'c' is first defined here.
[3]  obj.a = 123;
[4]  obj['e'] = 'f'
[5]  obj.e = obj['a']

As long as I get a symbol through square brackets or on initialization, I can easily store the K/V pair and create instance methods for the setters and getters.

However, I haven't been able to figure out how to create an object that will respond to 'c' if it's not defined, then do some magic. For instance,

My progress in the second style up to this point.

There's a very specific Object#respond_to_missing? which can take a specific symbol in it; but can't respond in the general case.

There's three possible ways that I can think of solving the commented out, second invocation style:

class A
  def b; puts 'c'; end
end
def always
  A.new
end

c = always
c.b

Given this, there may be a possible way to do some kind of error catching and then hook the object accordingly.

Anyway, if anyone has any idea here, it would be a pretty fun exercise I think. Thanks!

Upvotes: 2

Views: 232

Answers (2)

Richard
Richard

Reputation: 43

I have stumbled across this questions years later but still someone may find this useful.

The Hashie::Mash gem provides exactly this behaviour.

require 'hashie/mash'

obj = Hashie::Mash.new ({'a' => 'b'})
obj.c = 'd'
obj.a = 123
obj['e'] = 'f'
obj.e = obj['a']

p obj # #<Hashie::Mash a=123 c="d" e=123>

p obj.to_hash # {"a"=>123, "c"=>"d", "e"=>123}

It does also its magic upon assigning nested hashes and converts them to mashes:

require 'hashie/mash'

obj = Hashie::Mash.new ({'a' => 'b'})
obj.c = 3
obj.d =  { e: { f: {g: { h:  5 } } } }

puts obj.d.e.f.g.h # 5

puts obj.to_hash # {"a"=>"b", "c"=>3, "d"=>{"e"=>{"f"=>{"g"={"h"=>5}}}}}

The price here is the performance. In simple Measurement the Hashie::Mash is app. 10 times slower than Hash:

require 'benchmark'
require 'hashie/mash'

n = 1000000
time = Benchmark.measure do
  hash = {}
  (0..n).each do |key|
    hash[key] = key.to_s
  end
end
puts 'Standard Hash'
puts time

n = 1000000
time = Benchmark.measure do
  hash = Hashie::Mash.new
  (0..n).each do |key|
    hash[key] = key.to_s
  end
end
puts 'Hashie::Mash'
puts time

On my machine the results are:

Standard Hash
  0.330000   0.030000   0.360000 (  0.367405)
Hashie::Mash
  3.350000   0.040000   3.390000 (  3.394001)

Upvotes: 1

kristopolous
kristopolous

Reputation: 1848

Probably better ways of doing this:

class Hash
  def method_missing(symbol, opts = nil)
    string = symbol.to_s
    self[string[0..-2].to_sym] = opts if string[-1..-1] == '=' and opts
    self[symbol]
  end
end

If you want to subclass, than you can do this:

class JSHash < Hash
  def method_missing(symbol, opts = nil)
    string = symbol.to_s
    self[string[0..-2].to_sym] = opts if string[-1..-1] == '=' and opts
    self[symbol]
  end
end

class Hash
  def to_js
    JSHash.new.merge! self
  end
end

I'll keep this open for a while in case someone else has an idea of a better way here.

Upvotes: 2

Related Questions