09. Classes

So far, we've seen a lot of different kinds of objects, or classes: strings, integers, floats, arrays, and a few special objects (true, false, and nil), which we'll come back to later. In Ruby, these classes always start with a capital letter: String, Integer, Float, Array, etc. Generally, if we want to create a new object of a certain class, we use new:

a = Array.new  + [12345]  # Array addition.
b = String.new + 'hello'  # String addition.
c = Time.new

puts 'a = '+a.to_s
puts 'b = '+b.to_s
puts 'c = '+c.to_s

Because we can create arrays and strings using [...] and '...' respectively, we rarely use new for that (In any case, it's not very clear in the previous example that String.new creates an empty string and Array.new creates an empty array). Numbers, however, are an exception: you can't create an integer using Integer.new. You just have to type the number.

The Time Class

Okay, what about the Time class? Time objects represent moments in time. You can add (or subtract) numbers to (or from) times to get new times: adding 1.5 to a time returns a new time one and a half seconds later:

time  = Time.new    # The moment you loaded this page.
time2 = time + 60   # One minute later.

puts time
puts time2

You can also make a time for a specific moment using Time.mktime:

puts Time.mktime(2000, 1, 1)          # Year 2000.
puts Time.mktime(1976, 8, 3, 10, 11)  # Year I was born.

Note: when I was born, Pacific Daylight Time (PDT) was in effect. When the year 2000 came around, though, Pacific Standard Time (PST) was in effect, at least for us on the West Coast. The parentheses are to group the parameters to mktime. The more parameters you add, the more precise your time becomes.

You can compare two times using the comparison methods (an earlier time is less than a later time).

A Few Things to Try

  • One billion seconds... Find out the exact second you were born (if you can). Figure out when you will turn (or perhaps when you turned?) one billion seconds old. Then go mark it on your calendar.
  • Happy Birthday! Ask what year a person was born in. Then ask the month, and finally the day. Figure out how old they are and give them a SPANK! for each birthday they have had.

The Hash Class

Another very useful class is the Hash class. Hashes are a lot like arrays: they have a bunch of slots that can point to various objects. However, in an array, the slots are lined up in a row, and each one is numbered (starting from zero). In a hash, though, the slots aren't in a row (they are just sort of all together), and you can use any object to refer to a slot, not just a number. It's good to use hashes when you have a bunch of things you want to keep track of, but they don't really fit into an ordered list. For example, the colors I use in various parts of this tutorial:

colorArray = []  # same as Array.new
colorHash  = {}  # same as Hash.new

colorArray[0]         = 'red'
colorArray[1]         = 'green'
colorArray[2]         = 'blue'
colorHash['strings']  = 'red'
colorHash['numbers']  = 'green'
colorHash['keywords'] = 'blue'

colorArray.each do |color|
  puts color
end
colorHash.each do |codeType, color|
  puts codeType + ':  ' + color
end

If I use an array, I have to remember that slot 0 is for strings, slot 1 is for numbers, etc. But if I use a hash, it's easy! Slot 'strings' holds the color of strings, of course. Nothing to remember. You may have noticed that when I used each, the objects in the hash didn't come out in the same order I put them in (At least not when I wrote this. Maybe now they do... you never know with hashes). Arrays are for keeping things in order; hashes are not.

Although people usually use strings to name the slots in a hash, you can use any kind of object, even arrays and other hashes (though I can't think of a reason why you'd want to do that...):

weirdHash = Hash.new

weirdHash[12] = 'monkeys'
weirdHash[[]] = 'emptiness'
weirdHash[Time.new] = 'no time like the present'

Hashes and arrays are good for different things: it's up to you to decide which one is best for your particular problem.

Extending Classes

At the end of the last chapter, you wrote a method to return an English number string. However, that wasn't an integer method: it was just a generic program method. Wouldn't it be cooler if you could write 22.to_eng instead of englishNumber 22? Here's how you can do that:

class Integer
  def to_eng
    if self == 5
      english = 'five'
    else
      english = 'fifty-eight'
    end

    english
  end
end

# I'd better test it on a couple of numbers...
puts 5.to_eng
puts 58.to_eng

Well, I tested it; and nothing blew up. :)

We defined an integer method by just jumping into the Integer class, defining the method there, and jumping back out. Now all integers have this cool (incomplete) method. In fact, if you don't like the way the to_s method does things, you can just redefine it... but I don't recommend doing that! It's best to leave the old methods alone and make new ones when you need something new.

Confused yet? Let me go over that last program a bit more. So far, whenever we executed any code or defined a method, we did it in the default "program" object. In our last program, we left that object for the first time and went into the Integer class. We defined a method there (which made it an integer method) and all integers can use it. Inside that method, we use self to refer to the object (the integer) using the method.

Creating Classes

We've seen a lot of objects of different classes. However, it's easy to create kinds of objects that Ruby doesn't have. Luckily, creating a new class is just as easy as extending an existing one. Let's say we wanted to roll some dice in Ruby. Here's how we could make a Die class:

class Die

  def roll
    1 + rand(6)
  end

end

# Let's make a couple of dice...
dice = [Die.new, Die.new]

# ...and roll them.
dice.each do |die|
  puts die.roll
end

(If you skipped the section on random numbers, rand(6) just returns a random number between 0 and 5).

That's it! Objects of our very own. Roll the dice a few times (by hitting the "Refresh" button on your browser) and watch what happens.

We can define all sorts of methods for our objects... but there's something missing. Working with these objects hasn't changed much since we learned about variables. Look at our die, for example. Each time we roll it, we get a different number. But if we wanted to save that number, we'd have to create a variable to point to it. And any decent die should have a number, and rolling the die should change that number. If we hold onto the die, we don't have to keep track of the number it's showing.

However, if we try to store the number we rolled in a (local) variable inside roll, it will be gone as soon as roll finishes. We need to store that number in a different kind of variable:

Instance Variables

Normally when we talk about strings, we just call them strings. However, we could call them String Objects. Sometimes programmers might call them instances of the class String, but that's just a fancy (and rather long) way of saying string. An instance of a class is just an object of that class.

So, instance variables are just variables of an object. A method's local variables last until the method is finished. An object's instance variables, on the other hand, will last as long as the object does. To tell instance variables apart from local variables, they have an @ in front of their names:

class Die

  def roll
    @numberShowing = 1 + rand(6)
  end

  def showing
    @numberShowing
  end

end

die = Die.new
die.roll
puts die.showing
puts die.showing
die.roll
puts die.showing
puts die.showing

Very nice! Now roll rolls the die, and showing tells us which number is showing. But what if we try to look at what's showing before we've rolled the die (before we've defined @numberShowing)?

class Die

  def roll
    @numberShowing = 1 + rand(6)
  end

  def showing
    @numberShowing
  end

end

# Since I'm not going to use this die again,
# I don't need to save it in a variable.
puts Die.new.showing

Hmmm... Well, at least it didn't give us an error. Still, it doesn't make much sense for a "new" die to be nil. It would be nice if we could set up the die right when it's created. That's what initialize is for:

class Die

  def initialize
    # I'll just roll the die, though we could do something else
    # if we wanted to, like setting the die to have 6 showing.
    roll
  end

  def roll
    @numberShowing = 1 + rand(6)
  end

  def showing
    @numberShowing
  end

end

puts Die.new.showing

When an object is created, the initialize method (if it's defined) is always called.

Our die is almost perfect. The only thing missing is a way to set which side is showing... Why don't you write a cheat method that does just that? Come back when you're done (and when you've tested that it works, of course). Make sure that no one can make the die show a 7!

That was pretty cool. But it was still a bit of a toy example. Let me show you a more interesting example. Let's make a virtual pet, a baby dragon. Like most babies, it should be able to eat, sleep, and poop, which means we will need to be able to feed it, put it to bed, and take it for a walk. Internally, our dragon will need to keep track of if it is hungry, tired, or needs to go, but we won't be able to see that when we interact with it, just like you can't ask a human baby, "Are you hungry?". We'll also add a few other fun ways to interact with our baby dragon, and when he is born we'll give him a name. (Anything you pass into the new method is passed into the initialize method for you). Alright, let's give it a try:

class Dragon

  def initialize name
    @name = name
    @asleep = false
    @stuffInBelly     = 10  # He's full.
    @stuffInIntestine =  0  # He doesn't need to go.

    puts @name + ' is born.'
  end

  def feed
    puts 'You feed ' + @name + '.'
    @stuffInBelly = 10
    passageOfTime
  end

  def walk
    puts 'You walk ' + @name + '.'
    @stuffInIntestine = 0
    passageOfTime
  end

  def putToBed
    puts 'You put ' + @name + ' to bed.'
    @asleep = true
    3.times do
      if @asleep
        passageOfTime
      end
      if @asleep
        puts @name + ' snores, filling the room with smoke.'
      end
    end
    if @asleep
      @asleep = false
      puts @name + ' wakes up slowly.'
    end
  end

  def toss
    puts 'You toss ' + @name + ' up into the air.'
    puts 'He giggles, which singes your eyebrows.'
    passageOfTime
  end

  def rock
    puts 'You rock ' + @name + ' gently.'
    @asleep = true
    puts 'He briefly dozes off...'
    passageOfTime
    if @asleep
      @asleep = false
      puts '...but wakes when you stop.'
    end
  end

  private

  # "private" means that the methods defined here are
  # internal methods of the object. (You can feed
  # your dragon, but you can't ask him if he's hungry.)

  def hungry?
    # Method names can end with "?".
    # Usually, we only do this if the method
    # returns true or false, like this:
    @stuffInBelly <= 2
  end

  def poopy?
    @stuffInIntestine >= 8
  end

  def passageOfTime
    if @stuffInBelly > 0
      # Move food from belly to intestine.
      @stuffInBelly     = @stuffInBelly     - 1
      @stuffInIntestine = @stuffInIntestine + 1
    else  # Our dragon is starving!
      if @asleep
        @asleep = false
        puts 'He wakes up suddenly!'
      end
      puts @name + ' is starving! In desperation, he ate YOU!'
      exit  # This quits the program.
    end

    if @stuffInIntestine >= 10
      @stuffInIntestine = 0
      puts 'Whoops! ' + @name + ' had an accident...'
    end

    if hungry?
      if @asleep
        @asleep = false
        puts 'He wakes up suddenly!'
      end
      puts @name + '\'s stomach grumbles...'
    end

    if poopy?
      if @asleep
        @asleep = false
        puts 'He wakes up suddenly!'
      end
      puts @name + ' does the potty dance...'
    end
  end

end

pet = Dragon.new 'Norbert'
pet.feed
pet.toss
pet.walk
pet.putToBed
pet.rock
pet.putToBed
pet.putToBed
pet.putToBed
pet.putToBed

WOW! Of course, it would be much nicer if this was an interactive program, but you can do that part later. I was just trying to show you the parts directly related to creating a new Dragon class.

We saw a few new things in this example. The first is simple: exit quits the program right then and there. The second is the word private which we stuck right in the middle of our class definition. I could have left it out, but I wanted to enforce the idea that certain methods are things you can do to a dragon, and other methods are things that just happen within the dragon. You can think of these as "under the hood" things: unless you are an automobile mechanic, all you really need to know is how to use the gas pedal, the brake pedal, and the steering wheel. A programmer would call this the public interface.

Now for a more concrete example along those lines, let's talk about how you might represent a car in a video game (which is my line of work). First, you want to decide what your public interface looks like; in other words, which methods should people be able to call on your car objects? Well, they need to be able to push the gas pedal and the brake pedal, but they would also need to be able to specify how hard they are pushing the pedal (There's a big difference between flooring it and tapping it). They would also need to be able to steer, and again, say how hard they are turning the wheel. I suppose you could go further and add a clutch, turn signals, rocket launcher, back-mounted incinerator, flux capacitor, etc... depending on what kind of game you are making.

However, inside a car object, things are much more complex: other things a car would need are a speed, a direction, and a position (in the most basic sense). These attributes would be modified by pressing on the gas or brake pedals and turning the wheel, of course, but the user should not be able to set the position of the car directly (that would be like warping). You might also want to keep track of skidding or damage, air resistance, and so on. All of these things are internal to the car.

A Few Things to Try

  • Make an OrangeTree class. It should have a height method which returns its height, and a oneYearPasses method, which, when called, ages the tree one year. Each year the tree grows taller (however much you think an orange tree should grow in a year), and after some number of years (again, your call) the tree should die. For the first few years, it should not produce fruit, but after a while it should, and I guess that older trees produce more fruit... whatever you think makes the most sense. And, of course, you should be able to countTheOranges (which returns the number of oranges on the tree), and pickAnOrange (which reduces the @orangeCount by one and returns a string telling you how delicious the orange was, or else it just tells you that there are no more oranges to pick this year). Make sure that any oranges you don't pick one year fall off before the next year.
  • Write a program so that you can interact with your baby dragon. You should be able to enter commands like feed and walk, and those methods should be called on your dragon. Of course, since what you are inputting are strings, you will have to have some sort of method dispatch, where your program checks which string was entered and calls the appropriate method.

And that's it! But wait... I didn't tell you anything about classes that do things like send email, or save and load files on your computer, or how to create windows and buttons, or 3D worlds, or anything! Well, there are just so many classes you can use that I can't possibly show you all of them; I don't even know all of them. What I can tell you is where to find out more about them so you can look up the ones you want to use. But before I send you off, there is one more feature of Ruby that you really should know, something that most other languages don't have, but which I just can't live without: blocks and procs.