Recently I was using the shovel operator to concatenate a string and an instance variable exposed with an
attr_reader for use elsewhere and noticed an unexpected thing, the instance variable was being modified – admittedly, I was using the shovel operator poorly – but it prompted me to dig into some unexpected side effects of their use.
In Ruby it is convention to end methods that mutate the object they are called on end with an
!, for example
capitalize!. This isn’t always the case though, so some unexpected situations can crop up if you’re not careful (I’m looking at you
Because of how the shovel operator works, and other methods that mutate the object they’re called on, any method that exposes an object allows that object to be modified. So contrary to what you might expect, exposing an instance variable with a getter and no setter, doesn’t mean its safe from modification (ignoring the “devious” meta-programy means of modification).
Modifying instance variables through attr_reader
To demonstrate, we’ll take a few instance variables exposed only with getters, and modify them. To do so, create a class with an instance variable containing an array and a string and initialize it.
# create a class with an attr_reader for # a string and an array class Thing attr_reader :arr attr_reader :str def initialize @arr =  @str = "" end end # initialize the class thing = Thing.new
Putting the array and string return values as we would expect.
p thing.arr # =>  p thing.str # => ""
Now we can see that if we use a shovel with our getter methods, the state of the variables is altered.
p thing.arr << "hello" # => ["hello"] p thing.arr # => ["hello"] p thing.str << "hello" # => "hello" p thing.str # => "hello"
If you really need to “ensure” that instance variables aren’t inadvertantly modified, you can write a getter with
dup to return a copy of the original instance.
class Protected attr_accessor :exposed def initialize @exposed = "safe" @safe = "safe" end def safe @safe.dup end end # again, a sanity check to see that # our values both equal "safe" example = Protected.new p example.safe == example.exposed # => true # we'll shovel to try and modify them example.exposed << " or not" example.safe << " or not" # and test equality again p example.safe == example.exposed # => false # we can see @safe keeps its original value p example.safe # => "safe"
A gist with all the code is available here, please don’t hesitate to comment there if you have something to add.