Reading, Writing, Writing with a Reader, and maybe some Tests in Ruby
I wanted to explore some basic behavior in Ruby that seemed strange to me and from there maybe come up with a use-case where this behavior could be taken advantage of and applied. I first started thinking about this behavior in labs where an object has an instance variable that is initialized to an array. I’ll give an example with a different domain, but the idea is the same:
@pokemon = 
@pokemon << pokemon
Methods are the public interface for a class. Since I get to define their behavior, I want a new pokemon trainer to have to catch a pokemon using the #catch_pokemon method which stores a pokemon in the @pokemon instance variable array. The class only provides an attr_reader for the instance variable because I want to be able to read back a trainer’s pokemon but do not want a user to write to that variable outside of the interface of #catch_pokemon. I don’t want or provide a #pokemon= method. But then it turns out I can do something like this:
Without ever calling #catch_pokemon I was able to add squirtle to the list of Trainer Jeff’s pokemon. Is this just something weird with arrays and the shovel << method? Let’s try something else, I’ll make another class that specifically only has an attr_reader for some instance variable and not an attr_writer, which in theory will stop a user from writing to that variable.
@name = "Tic Tac"
Great, a really useful class that produces dog objects with a name attribute that is initialized to “Tic Tac” and doesn’t provide any methods for updating the name. Alright, so lets play around in IRB…
… and it all makes sense; tictac remains Tic Tac. But then I’m able to do something like this:
I can even force my way in there and change the name:
Tic Tac is now named “Popsicle” and I was able to write that change without a name= method. Weird. This seemed really puzzling and also felt like a big hack. Ruby’s flexiblity or casual-ness seemed maybe too casual, and I felt like I arrived at some weird behavior.
From here I want to branch out and cover 3 things:
- What’s happening/how to change this this behavior if so desired
- Realize/explore that this ‘hack’ is actually built in to Ruby
- Maybe try to come up with a scenario where you could use this behavior to your advantage.
What’s going on? / is there any other way?:
When method calls (i.e. clear, =, shovel) were chained on to the return value of the attr_reader method they altered that value permanently or destructively. This means that the empty string returned from dog.name.clear (=> “”) must be the same String object as the initial call to dog.name (=> “Tic Tac”) returned.
The #name method returns the actual object stored in the instance variable @name. It’s like there’s an implicit ‘!’ on the method. Remember that this method gets defined by the attr_reader macro which just writes a boilerplate method definition for us that looks like this:
If instead #name returned a copy of the object, then a user could do anything they wanted to the copy, which would have the same value as the original object, but the object stored at the instance variable would remain unchanged. In ruby you can copy an object using #dup.
To put it all together, let’s replace the attr_reader macro with our own definition of the #name method.
@name = "Tic Tac"
and test it in IRB…
Syntactic Vinegar -or- ‘a feature not a bug’:
To ‘hack’ into tictac’s @name attribute we needn’t even have to go to the trouble of tictac.name.clear= ‘whateverrr’ . Ruby makes it easy for us:
#instance_variable_set and the reciprocal #instance_variable_get are instance methods of Ruby’s basic Object class that all your classes inherit from. They have an intentionally clunky and awkward syntax because they violate the principle of encapsulation. Even the official Ruby documentation calls them frustrating.
The obvious questions being Why? and When?
A Use Case?
So the answer is I don’t know.
Cursory research into why these methods exist brought unclear results. One probably good example was in a kind of meta-programming way where you could dynamically generate the instance variables of an instance. With my more limited experience, the only example I could come up with where this functionality would be helpful and useful was in writing Rspec tests for interdependent methods. And even here stack overflow says maybe this is not the best practice but then in the same page someone says its ok and/or is ambivalent.
Here in the “trigger” phase in my test for the #genres query-type method I am forced to call the #add_genre command-type method to populate an instance of a Movie. This could be problematic if #add_genres was buggy or non-functional. I would have no way to isolate #genres and test if it was doing what was expected. I could refactor to something like:
Now #genres could be tested in isolation without calling the command-type method #add_genres. Thanks Ruby!