2

Actually I want to use such constructs in chef attributes, where I initialize a structure with a constant and modify it

init_value = { "a" => { "b" => "c" } }
prepare = init_value
prepare["a"]["x"] = "y"

now init_value also contains ["a"]["x"] = "y", so when I prepare a new value

prepare = init_value
prepare["a"]["y"] = "x"

so prepare["a"] contains the keys ["b", "x", "y"].

How can I initialize prepare with a constant without quoting the constant, so that in the last step, prepare["a"] only contains the two keys ["b","y"]?

ikrabbe
  • 1,909
  • 12
  • 25
  • 1
    seems that the only practical way is `prepare = Marshal.load(Marshal.dump(init_value))`. I wouldn't even call this copy. It seems that ruby totally lacks a real "deep copy" function for hashes. If you know a method please propose one! – ikrabbe May 23 '17 at 16:14
  • Marshal is, unfortunately, the way to do a deep copy. Because even `dup` and then `freeze` will not stop modification of the values themselves, only assignment to keys. – Kris May 23 '17 at 16:19

3 Answers3

3

Extracted from Rails 4.2.7

Implementation:

class Object
  def duplicable?
    true
  end
  def deep_dup
    duplicable? ? dup : self
  end
end 

class Hash
   def deep_dup
     each_with_object(dup) do |(key, value), hash|
       hash[key.deep_dup] = value.deep_dup
     end
  end
end

class Array
  def deep_dup
    map { |it| it.deep_dup }
  end
end

# Not duplicable? 
# if ruby version < 2.0 also add Class and Module as they were not duplicable until 2.0
[Method, Symbol, FalseClass, TrueClass, NilClass, Numeric, BigDecimal].each do |m|
  m.send(:define_method, :duplicable?, ->{false})
end

Then you could use a method for init_value so that deep_dup is always called and you can't accidentally forget

#since you asked for a constant
INIT_VALUE = { "a" => { "b" => "c" } }.freeze

def init_value 
  INIT_VALUE.deep_dup 
end

And usage as such

prepare = init_value
prepare["a"]["x"] = "y"

prepare2 = init_value
prepare2["a"]["y"] = "x"

prepare
#=> {"a"=>{"b"=>"c", "x"=>"y"}}
prepare2
#=> {"a"=>{"b"=>"c", "y"=>"x"}}
engineersmnky
  • 25,495
  • 2
  • 36
  • 52
  • This looks like a reasonable answer. I will try it tomorrow and approve if it works for me. – ikrabbe May 23 '17 at 16:54
  • 1
    @ikrabbe depending on your implementation checkout Stefan's answer as it provides the same functionality with none of the code. That being said I will leave this hear as an example of "deep copying" since it seemed like a hot topic on this post. – engineersmnky May 23 '17 at 20:58
3

You could move the initial hash into a method. This way, the method always returns a "fresh" hash:

def init_value
  {"a"=>{"b"=>"c"}}
end

prepare = init_value
prepare["a"]["x"] = "y"
prepare
#=> {"a"=>{"b"=>"c", "x"=>"y"}}

prepare = init_value
prepare["a"]["y"] = "x"
prepare
#=> {"a"=>{"b"=>"c", "y"=>"x"}}
Stefan
  • 109,145
  • 14
  • 143
  • 218
  • This is by far the simplest solution (I can't believe I overlooked this) I was so focused on the fact that the OP and other commenters seemed sure deep copying was not something that was supported. – engineersmnky May 23 '17 at 20:53
  • Thanks to you all! I think we actually found what I and possibly some more have been searching for! Good Teamwork @engineersmnky – ikrabbe May 24 '17 at 08:43
0

I think you want a "deep copy" of init_value when assigning to prepare.

see: How do I copy a hash in Ruby?

jdizzle
  • 4,078
  • 1
  • 30
  • 38
  • please add an example of how to do it, then I will vote for your answer. Actually the answer you quoted does not contain usefull examples. I gave a minimal example that can easily be converted into a code driven answer, as Ruby actually seems to lack a real deep copy function. – ikrabbe May 23 '17 at 16:03