I have some data saved in deeply nested Hashes and Arrays and I have run into trouble with the text encoding of the data. I know for fact that the texts are encoded in "UTF-8", so I decided to go over each element and force the encoding.
So, I created a method called deep_each
for the Enumerable
module:
module Enumerable
def deep_each(&block)
self.each do |element|
if element.is_a? Enumerable then
element.deep_each(&block)
else
block[element]
end
end
end
end
And expected to be able to fix the data using the following method call:
deephash.deep_each {|element| element.force_encoding("UTF-8") if element.class == String}
But the result was disappointing:
deephash.deep_each {|element| element.force_encoding("UTF-8") if element.class == String}
> RuntimeError: can't modify frozen String
> from (pry):16:in `force_encoding'
Then I moved the function down the hierarchy, to the "Array" and "Hash" classes:
class Hash
def deep_each(&block)
self.each do |element|
if [Array, Hash].include? element.class then
element.deep_each(&block)
else
block[element]
end
end
end
end
class Array
def deep_each(&block)
self.each do |element|
if [Array, Hash].include? element.class then
element.deep_each(&block)
else
block[element]
end
end
end
end
Surprisingly, the same call works now.
What constraint am I violating here, and how can I define a method for all Enumerables without defining it for every single one of them?
As far as I can tell, you should get the exact same error with both your Enumerable version and your Array/Hash monkey patch. I do. Are you sure you're using the same deephash
in both cases?
Normally when you loop each
on a hash, you'd pass in both key and value to the block. You're passing a single value element
to the block. This then is an Array with the key and value:
irb> {a:1, b:2}.each {|el| puts el.inspect }
[:a, 1]
[:b, 2]
Your deep_each
checks if this is an Enumerable, and it is, so it calls deep_each
on the list. Then, finally, you reach the leafs and call the block on the key and the value. The block checks if it's working with a String, and if so, forces encoding.
If your hash key is a string, you will try to mutate it. But hash keys are frozen, and so RuntimeError: can't modify frozen String
is raised.
irb> {a: {b: {c: "abc"}}}.deep_each { |el| el << "efg" if String === el}
=> {:a=>{:b=>{:c=>{:d=>"abcefg"}}}}
irb> {a: {b: {"c" => "abc"}}}.deep_each { |el| el << "efg" if String === el}
RuntimeError: can't modify frozen String