28

Say I have this hash:

entry = {"director"=>"Chris Nolan", "producer"=>"Sum Duk", "writer"=>"Saad Bakk"}

I want to extract each key into its own local variable with the associated value:

director = "Chris Nolan"
producer = "Sum Duk"
...

By using a loop and not:

director = entry["director"]

Since there are a lot of values and I don't want to do them individually.

I found this which works almost perfectly except it creates an instance variable and I want a local variable, but local_variable_set doesn't exist for some reason.

entry.each_pair { |k, v| instance_variable_set("@#{k}", v) }

Is there a solution? Or failing that, a way to turn an instance variable into a local one and delete the instance one without doing it one by one?

Christopher Oezbek
  • 23,994
  • 6
  • 61
  • 85
kakubei
  • 5,321
  • 4
  • 44
  • 66
  • Any reason they can't stay in the hash and be accessed from there? Presumably, you're using the local variables later - wouldn't it be just as easy to use the hash values? – Pavling Nov 14 '12 at 18:11
  • If you're using Rails (or just ActiveSupport), you can use the following one-liner: `director, producer, writer = entry.values_at('director', 'producer', 'writer')`. It's unfortunate that you have to type each variable name twice (instead of zero times as you asked for), but it is still the most concise way I've found to do this. – antinome Aug 14 '15 at 14:40
  • possible duplicate of [How to dynamically create a local variable?](http://stackoverflow.com/questions/18552891/how-to-dynamically-create-a-local-variable) – Ajedi32 Aug 14 '15 at 16:05

4 Answers4

11

Option 1

I can't recommend this except for fun, but it mostly has the effect you are after:

entry.each |k, v|
  singleton_class.send(:attr_accessor, k)
  send("#{k}=", v)
end

director                        # => "Chris Nolan"
self.director = "Wes Anderson"  # Unfortunately, must use self for assignment
director                        # => "Wes Anderson"

Instead of creating local variables it defines acccessor methods on the singleton class of the current object, which you can call as if they were local variables.

To make them more "local" you could use singleton_class.remove_method to remove these methods when you are done with them. You could even try to alias any existing singleton class methods with the same name and restore them afterwards.

Option 2

This is something I use in real code. It requires the ActiveSupport gem which comes with Ruby On Rails but can also be used on its own.

director, producer, writer = entry.values_at('director', 'producer', 'writer')

Unfortunately it requires typing each variable name twice, instead of zero times as you asked for. If you were certain about the order of the values in the hash, you could write:

director, producer, writer = entry.values  # Not so good, IMO.

I feel uneasy about this version because now the code that created the hash has a non-obvious responsibility to ensure the hash is in a certain order.

Note

If your goal is just to reduce typing, here is an approach that only requires two more characters per variable access than if they were true local variables:

e = OpenStruct.new(entry)
e.director     # => "Chris Nolan"
antinome
  • 3,408
  • 28
  • 26
4

You can't create local variables, because of variable scope.
If you create a local variable inside a block, the variable would be only accessible inside the block itself.
Please refer to this question for more info.
Dynamically set local variables in Ruby

Community
  • 1
  • 1
MurifoX
  • 14,991
  • 3
  • 36
  • 60
4

You can do this with eval, but all operations on those variables must be inside the scope they were defined in.

For example, this will work:

vals = {
  "foo" => "bar",
  "baz" => "qux"
}

eval <<-EOF
  #{ vals.map {|k, v| "#{k} = \"#{v}\""}.join("\n") }
  puts foo
EOF

However, this will not:

vals = {
  "foo" => "bar",
  "baz" => "qux"
}

eval <<-EOF
  #{ vals.map {|k, v| "#{k} = \"#{v}\""}.join("\n") }
EOF

puts foo

as foo goes out of scope at the end of the eval. However, if all your work on the variables can be done inside the scope of the eval, it's quite doable.

Chris Heald
  • 61,439
  • 10
  • 123
  • 137
  • @kakubei: Exactly, in Ruby you can break through almost any constraint. Although the one you chose - local variables - is a bit difficult, local variables are only meant for small time use. – Boris Stitnicky Nov 14 '12 at 16:19
  • That doesn't really work for hashes with non-string (or stringable) keys. – Hut8 Jul 11 '13 at 00:43
0

Expanding on @antinome's note: If you want local variables just to exist, because you want to access them dynamically for instance by interpolating them in a String, then OpenStruct + instance_eval offers this powerful way:

require 'ostruct'
o = OpenStruct.new(entry)
o.instance_eval("Directed by #{director}, produced by #{producer}")
o.instance_eval {
  puts "Directed by #{director}"
  puts "Produced by #{producer}"
}
Christopher Oezbek
  • 23,994
  • 6
  • 61
  • 85
  • I would prefer `instance_exec` over `instance_eval` – Nick Roz Oct 26 '22 at 16:42
  • @NickRoz Why would it matter in general and in this case? – Christopher Oezbek Oct 26 '22 at 21:33
  • 1
    In that very case I guess there is no difference, but I stick to it as a general recommendation to avoid temptation to pass a string inside (just like in the 3rd line of code) from parameters / user intput / database, thus it is relatively easy to get code injection. Whereas _exec is limited to blocks only, that at least protects you from a mistake. Apart from this it is underestimated answer – Nick Roz Oct 27 '22 at 10:08