def things_n_stuff

Adventures in Code

Scope, Safe Ignorance, and Passing Objects

Today’s post is on something that was confusing to me and is probably confusing to other budding ruby developers. 100% of the credit for any understanding is owed to a combination of Avi, Jeffrey Baird, Sandi Metz, a definition I found online from the Dave Thomas ruby book, and osmosis from classmates. Unless something’s wrong, in which case it’s 100% me.

Ah Befuddlement, I Know ye Well

Anyone who’s dabbled in ruby and tried to understand Object Oriented Programming must understand scope. Everything in ruby is an object, and there are limitations (or at least should be) to what kinds of other things each object has access to and can affect. One way to visualize this is as an electrical outlet.

If you only have one thing plugged into an outlet, like a drill, it’s simple to remove that plug or put it in a different socket if you subsequently want to plug in a chainsaw. (People use electric chainsaws, right?) On the other hand, if your outlet is chocked with a million plugs and extension cords it get trickier to find the cord you want, and if you try to change the setup later it’s easy to unplug your monitor when you thought you were pulling on the lamp chord.

Proper Ignorance vs Entanglements

Ruby object are somewhat similar and a million times more elegant. A ruby object needs to know/have access to only the stuff it should, in order to do its job, nothing more, nothing less. This is what prevents unnecessary entanglements, and allows you (and me) to build awesome, powerful modularity into programs, and also what inclines rubyists towards the convention of building numerous small, single-purpose, clearly defined and clearly labeled methods instead of larger, more-comprehensive ones. Eluminating this way of building is Metz’s ballywick; check out her talk: http://www.poodr.info/blog. (Also a super nice and interesting lady.)

Scope-arama

One first speedbump in learning to build and think this way is both obvious and surprizing when you first arrive at it: if I’m building wonderful, clean, isolated methods and single-purpose objects, how can I get anything done? If I have an object/message in a Spaceship class, how can I get it over to an object of the GroundControl class? Elegantly, of course, and many different ways. Ruby’s about choice! However you choose to do it though, the process relies on the concept of approriately ignorant object, and scope.

Your Ruby is Haunted

One key to understanding scope is understanding self. (…or finding Dave Thomas’ explanations of self somewhere online.) In ruby the idea of ‘self’ is attached to whatever part of your program is currently being processed. If you create an instance of a class and give it an attribute, the only one who will know the value of that attribute is the instance itself.

How to Pass a Pirate

To demo this way of getting messages between classes I’ll walk through an example I found very helpful, given to me by the super-fantabulous Jeffrey Baird – a teacher here at the Flatiron School. Definitely good for beginners, but probably not absolute beginners. I’m also going to skip ahead to the relevant part.

Setup: make 4 files, pirate_test.rb, ship_test.rb, and pirate.rb and ship.rb. (download the gem if you like, or run a different test-suite.)Copy the test content into the appropriate file, run, add code to the appropriate file, run, repeat until you get the tests to pass.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#ship_test.rb
require 'fis/test'
require_relative 'pirate3'
require_relative 'ship3'
include Fis::Test

test 'should instantiate a ship' do
  assert Ship.new
end

test 'a ship should know all the pirates on it' do
  pirate =Pirate.new
  ship = Ship.new
  ship.name = "Black Pearl"

  pirate.ship = ship

  assert ship.pirates.include?(pirate)
  
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#pirate_test.rb
require 'fis/test'
require_relative 'ship3'
require_relative 'pirate3'

include Fis::Test

test "should instantiate a pirate class" do
  assert Pirate.new
end

test "pirate should have a name" do
  pirate = Pirate.new
  pirate.name = "Dan"
  assert_equal pirate.name, "Dan"
end

test "pirate should know what ship it belongs to" do
  pirate = Pirate.new
  ship = Ship.new
  pirate.ship = ship 

  assert_equal pirate.ship, ship
  
end
1
2
3
4
5
6
7
8
9
10
11
#ship.rb
class Ship
  def name=(name)
    @name = name
  end

  def name
    @name
  end

end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#pirate.rb
class Pirate

  def name= name
    @name = name
  end

  def name
    @name
  end

  def ship= ship
    @ship = ship
  end

  def ship
    @ship
  end
  
end

At this point your pirate tests should be passing, and running your ship_test.rb file is giving you this:

ruby ship_test.rb 
pass: should instantiate a ship
fail: a ship should know all the pirates on it

So. How do you get a ship to know all of the pirates on it? Here’s where we need to rexamine the notion of self and scope. We want to be able to ask a ship, ‘Hey ship, who are all of your pirates?’ So first we’ll want the Ship class to keep a list of all the pirates available to the class. Let’s use a constant to store an array of all pirates. Then we’ll give every instance a(n instance) method that will simply show all the pirates.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#ship.rb
class Ship

PIRATES =[]

  def name=(name)
    @name = name
  end

  def name
    @name
  end

  def pirates
    PIRATES
  end

end

Now our Ship class has an empty list setup to store pirates, and you can call pirates on any ship instance and get that list. But how do we populate the list? The Ship class doesn’t have access to any pirates, doesn’t know their name, but we need to get them on the ships’ instances. Instances of Ship also don’t know any of the pirates. I need to gets me some pirates into the PIRATES constant in the ship class, hopefully without creating any new unnecessary connections between the classes. Let’s check out the Pirate class again.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Pirate

  def name= name
    @name = name
  end

  def name
    @name
  end

  def ship= ship
    @ship = ship
  end

  def ship
    @ship
  end

end

What references do we have to ship? Well, we have a lovely getter and setter for adding a ship as an attribute for pirate instances, ship= and ship. But what do these methods “know about?” If we’ve made a pirate instance called johnny, when we call johnny.ship = “The Sea Bitch”, self is johnny and we’re passing the ship name “The Sea Bitch” to the johnny instance. At that moment we’ve connected some information from the Ship class (a ship.name) and an object from the Pirate class. The argument passed to ship= is an instance of Ship that was already created and given a name. At the same ‘moment’, ie. while self is passing through the ship= method you can piggy-back on the values present. How?

The key is realizing that we have a ship.pirates method, and remembering that when you call pirate.ship=, self is a pirate instance and the passed-argument is a ship instance. Key: as soon as that ship instance arrives into the method and its reciever, the method has the ability to call the appropriate Ship class’s methods on the ship instance. You want to give it the current instance of pirate, so give it SELF! Modify the ship= method:

1
2
3
4
  def ship=(ship)
    @ship = ship 
    ship.pirates << self
  end

It’s that clean! What’s going on? Basically we alter the method to allow us to do pirate.ship=. So we can set the ship for a pirate. Then, assigning the ship instance passed in as an argument to the @ship instance variable allows us to set the name of the ship for the instance. ship.pirates << self allows us to add the instance that ship= is being called on (an instance of pirate) to THE RESULT of callin the pirates method on an instance of ship.

What can get confusing is that it seems like we’re playing around in the Pirate class to add something to the Ship class, but what we’re actually doing is telling an instance of the Pirate class to add itself to the PIRATES list inside the ship class, as soon as it is assigned a ship attribute. We’re using objects and their attributes to communicate with other objects and atributes. And we’re keeping objects decently ignorant about all things besides their appropriate attributes.

Cool.