08. Writing Your Own Methods

As we've seen, loops and iterators allow us to do the same thing (run the same code) over and over again. However, sometimes we want to do the same thing a bunch of times, but from different places in the program. For example, let's suppose we are writing a questionnaire program for a psychology student. Taking into account the psychology students I know and the questionnaires they provided me, it would be something like this:

puts 'Hello, and thank you for taking the time to help'
puts 'me with this experiment. My experiment has to do'
puts 'with the way people feel about Mexican food.'
puts 'Just think about Mexican food and try to answer'
puts 'every question honestly, with either a "yes" or a "no".'
puts 'My experiment has nothing to do with bed-wetting.'
puts

# We ask these questions, but we ignore their answers.

goodAnswer = false
while (not goodAnswer)
  puts 'Do you like eating tacos?'
  answer = gets.chomp.downcase
  if (answer == 'yes' or answer == 'no')
    goodAnswer = true
  else
    puts 'Please answer "yes" or "no".'
  end
end

goodAnswer = false
while (not goodAnswer)
  puts 'Do you like eating burritos?'
  answer = gets.chomp.downcase
  if (answer == 'yes' or answer == 'no')
    goodAnswer = true
  else
    puts 'Please answer "yes" or "no".'
  end
end

# We pay attention to *this* question, though.
goodAnswer = false
while (not goodAnswer)
  puts 'Do you wet the bed?'
  answer = gets.chomp.downcase
  if (answer == 'yes' or answer == 'no')
    goodAnswer = true
    if answer == 'yes'
      wetsBed = true
    else
      wetsBed = false
    end
  else
    puts 'Please answer "yes" or "no".'
  end
end

goodAnswer = false
while (not goodAnswer)
  puts 'Do you like eating chimichangas?'
  answer = gets.chomp.downcase
  if (answer == 'yes' or answer == 'no')
    goodAnswer = true
  else
    puts 'Please answer "yes" or "no".'
  end
end

puts 'Just a few more questions...'

goodAnswer = false
while (not goodAnswer)
  puts 'Do you like eating sopapillas?'
  answer = gets.chomp.downcase
  if (answer == 'yes' or answer == 'no')
    goodAnswer = true
  else
    puts 'Please answer "yes" or "no".'
  end
end

# Ask lots of other questions about Mexican food.

puts
puts 'DEBRIEFING:'
puts 'Thank you for taking the time to help with'
puts 'this experiment. In fact, this experiment'
puts 'has nothing to do with Mexican food. It is'
puts 'an experiment about bed-wetting. The Mexican'
puts 'food was just there to catch you off guard'
puts 'in the hopes that you would answer more'
puts 'honestly. Thanks again.'
puts
puts wetsBed

A nice long program, with a lot of repetition. (All the sections of code around questions about Mexican food are identical, and the bed-wetting question is slightly different.) Repetition is a bad thing. But we can't make a big iterator, because sometimes we want to do something between the questions. In situations like this, it's best to write a method. Here's how:

def sayMoo
  puts 'mooooooo...'
end

Uh... Our program didn't say mooooooo... Why not? Because we didn't tell it to. We only told it how to say mooooooo..., but we never told it to actually do it. Let's give it another shot:

def sayMoo
  puts 'mooooooo...'
end

sayMoo
sayMoo
puts 'coin-coin'
sayMoo
sayMoo

Ah! Much better. (In case you don't speak French, that was a French duck in the middle of the program. In France, ducks say "coin-coin").

So, we defined the method sayMoo. (Method names, like variable names, start with a lowercase letter. There are exceptions, like + or ==). But don't methods always have to be associated with objects? Well, yes, they do. And in this case (just like with puts and gets), the method is associated only with the object representing the program as a whole. In the next chapter we'll see how to add methods to other objects. But first...

Method Parameters

You may have noticed that some methods (like gets, or to_s, or reverse...) can be called just on an object. However, other methods (like +, -, puts...) take parameters to tell the object what to do with the method. For example, you don't just say 5+, right? You're telling 5 to add, but you're not telling it what to add.

To add a parameter to sayMoo (the number of moos, for example), we can do the following:

def sayMoo numberOfMoos
  puts 'mooooooo...'*numberOfMoos
end

sayMoo 3
puts 'oink-oink'
sayMoo  # This will give an error because no parameter was passed.

numberOfMoos is a variable that points to the parameter that was passed. I'll say it again, but it's a bit confusing: numberOfMoos is a variable that points to the parameter that was passed. So if I type sayMoo 3, the parameter is 3, and the variable numberOfMoos points to 3.

As you can see, now the parameter is required. After all, what sayMoo is supposed to do is multiply 'mooooooo' by a number. But by how much, if you didn't say? Your computer has no idea.

If we compare objects in Ruby to nouns in English, methods can similarly be compared to verbs. Thus, you can think of parameters as adverbs (like in sayMoo, where the parameter tells us how to sayMoo) or sometimes as direct objects (like in puts, where the parameter is what puts will print).

Local Variables

In the following program, there are two variables:

def doubleThis num
  numTimes2 = num*2
  puts num.to_s+' doubled is '+numTimes2.to_s
end

doubleThis 44

The variables are num and numTimes2. Both are located inside the method doubleThis. These (and all other variables you've seen so far) are local variables. This means they live inside the method and cannot leave. If you try, you'll get an error:

def doubleThis num
  numTimes2 = num*2
  puts num.to_s+' doubled is '+numTimes2.to_s
end

doubleThis 44
puts numTimes2.to_s

Undefined local variable... Actually, we did define that local variable, but it's not local relative to where we tried to use it; it's local to the method.

This might seem inconvenient, but it's actually very good. While you don't have access to variables inside methods, this also means no one has access to your variables, and that means no one can do something like this:

def littlePest var
  var = nil
  puts 'HAHA! I ruined your variable!'
end

var = 'You can\'t touch my variable!'
littlePest var
puts var

There are actually two variables in that little program named var: one inside the littlePest method and one outside it. When you called littlePest var, we really just passed the string that was in var to the other one, so they were pointing to the same string. Then the littlePest method pointed its local var to nil, but that didn't do anything to the var outside the method.

Return Values

You may have noticed that some methods give something back when you call them. For example, the gets method returns a string (the string you typed), and the + method in 5+3 (which is actually 5.+(3)) returns 8. Arithmetic methods for numbers return numbers, and arithmetic methods for strings return strings.

It is important to understand the difference between methods returning a value to where it was called, and your program generating output to your screen, as puts does. Note that 5+3 returns 8; it does not print 8 to the screen.

So what does puts return? We never cared before, but let's take a look now:

returnVal = puts 'This puts returned:'
puts returnVal

The first puts returned nil. Although we didn't test the second puts, it did the same thing; puts always returns nil. Every method has to return something, even if it's just nil.

Take a break and write a program that finds out what the sayMoo method returned.

Are you surprised? Well, here's how it works: the return value of a method is simply the last line evaluated in the method. In the case of the sayMoo method, this means it returned 'puts mooooooo...'*numberOfMoos, which is just nil, since puts always returns nil. If we wanted all our methods to return the string 'yellow submarine', we'd just have to put it at the end of them:

def sayMoo numberOfMoos
  puts 'mooooooo...'*numberOfMoos
  'yellow submarine'
end

x = sayMoo 2
puts x

Now let's try that psychology survey again, but this time we'll write a method to ask the questions for us. It will need to take the question as a parameter and return true if the answer was yes and false if the answer was no. (Even if we ignore the answer most of the time, it's a good idea to have the method return the answer. That way we can use it for the bed-wetting question). I'm also going to shorten the greeting and the debriefing, just to make it easier to read:

def ask question
  goodAnswer = false
  while (not goodAnswer)
    puts question
    reply = gets.chomp.downcase

    if (reply == 'yes' or reply == 'no')
      goodAnswer = true
      if reply == 'yes'
        answer = true
      else
        answer = false
      end
    else
      puts 'Please answer "yes" or "no".'
    end
  end

  answer # This is what we return (true or false).
end

puts 'Hello, and thank you for...'
puts

ask 'Do you like eating tacos?'      # We ignore this return value.
ask 'Do you like eating burritos?'
wetsBed = ask 'Do you wet the bed?'  # We save this return value.
ask 'Do you like eating chimichangas?'
ask 'Do you like eating sopapillas?'
ask 'Do you like eating tamales?'
puts 'Just a few more questions...'
ask 'Do you like drinking horchata?'
ask 'Do you like eating flautas?'

puts
puts 'DEBRIEFING:'
puts 'Thank you for...'
puts
puts wetsBed

Not bad, huh? We can add more questions (and adding more questions is easy now), but our program stays short! This is huge progress — every lazy programmer's dream.

One More Big Example

I think another example method would be very useful here. Let's call this one englishNumber. This method will take a number, like 22, and return the English version of it (in this case, the string 'twenty-two'). For now, let's limit it to integers between 0 and 100.

(NOTE: This method uses a new trick to return from a method early using the return keyword, and introduces a new concept: elsif. This should be clear in context).

def englishNumber number
  # We only want numbers between 0 and 100.
  if number < 0
    return 'Please enter a number that isn\'t negative.'
  end
  if number > 100
    return 'Please enter a number that is 100 or less.'
  end

  numString = ''  # This is the string we will return.

  # "left" is how much of the number we still have left to write out.
  # "write" is the part we are writing out right now.
  left  = number
  write = left/100          # How many hundreds left to write out?
  left  = left - write*100  # Subtract off those hundreds.

  if write > 0
    return 'one hundred'
  end

  write = left/10           # How many tens left to write out?
  left  = left - write*10   # Subtract off those tens.

  if write > 0
    if ((write == 1) and (left > 0))
      # Since we can't write "tenty-two" instead of "twelve",
      # we have to make a special exception for these.
      if left == 1
        numString = numString + 'eleven'
      elsif left == 2
        numString = numString + 'twelve'
      elsif left == 3
        numString = numString + 'thirteen'
      elsif left == 4
        numString = numString + 'fourteen'
      elsif left == 5
        numString = numString + 'fifteen'
      elsif left == 6
        numString = numString + 'sixteen'
      elsif left == 7
        numString = numString + 'seventeen'
      elsif left == 8
        numString = numString + 'eighteen'
      elsif left == 9
        numString = numString + 'nineteen'
      end
      # Since we took care of the digit in the ones place already,
      # we have nothing left to write.
      left = 0
    elsif write == 1
      numString = numString + 'ten'
    elsif write == 2
      numString = numString + 'twenty'
    elsif write == 3
      numString = numString + 'thirty'
    elsif write == 4
      numString = numString + 'forty'
    elsif write == 5
      numString = numString + 'fifty'
    elsif write == 6
      numString = numString + 'sixty'
    elsif write == 7
      numString = numString + 'seventy'
    elsif write == 8
      numString = numString + 'eighty'
    elsif write == 9
      numString = numString + 'ninety'
    end

    if left > 0
      numString = numString + '-'
    end
  end

  write = left  # How many ones left to write out?
  left  = 0     # Subtract off those ones.

  if write > 0
    if write == 1
      numString = numString + 'one'
    elsif write == 2
      numString = numString + 'two'
    elsif write == 3
      numString = numString + 'three'
    elsif write == 4
      numString = numString + 'four'
    elsif write == 5
      numString = numString + 'five'
    elsif write == 6
      numString = numString + 'six'
    elsif write == 7
      numString = numString + 'seven'
    elsif write == 8
      numString = numString + 'eight'
    elsif write == 9
      numString = numString + 'nine'
    end
  end

  if numString == ''
    # The only way "numString" could be empty is if
    # "number" is 0.
    return 'zero'
  end

  # If we got this far, then we had a number somewhere
  # in between 0 and 100, so we need to return "numString".
  numString
end

puts englishNumber(  0)
puts englishNumber(  9)
puts englishNumber( 10)
puts englishNumber( 11)
puts englishNumber( 17)
puts englishNumber( 32)
puts englishNumber( 88)
puts englishNumber( 99)
puts englishNumber(100)

Well, there are a few things I don't like about this program. First: there's too much repetition. Second: it doesn't handle numbers greater than 100. Third: there are too many special cases, too many returns. Let's use some arrays and try to clean it up:

def englishNumber number
  if number < 0  # No negative numbers.
    return 'Please enter a number that is not negative.'
  end
  if number == 0
    return 'zero'
  end

  # No more special cases! No more returns!

  numString = ''  # This is the string we will return.

  onesPlace = ['one',     'two',       'three',    'four',     'five',
               'six',     'seven',     'eight',    'nine']
  tensPlace = ['ten',     'twenty',    'thirty',   'forty',    'fifty',
               'sixty',   'seventy',   'eighty',   'ninety']
  teenagers = ['eleven',  'twelve',    'thirteen', 'fourteen', 'fifteen',
               'sixteen', 'seventeen', 'eighteen', 'nineteen']

  # "left" is how much of the number we still have left to write out.
  # "write" is the part we are writing out right now.
  left  = number
  write = left/100          # How many hundreds left to write out?
  left  = left - write*100  # Subtract off those hundreds.

  if write > 0
    # Now here's a really sly trick:
    hundreds  = englishNumber write
    numString = numString + hundreds + ' hundred'
    # That's called "recursion". So what did I just do?
    # I told this method to call itself, but with "write" instead of
    # "number". Remember that "write" is (at the moment) the number of
    # hundreds we have to write out. After we add "hundreds" to
    # "numString", we add the string ' hundred'. So, for example, if
    # we originally called englishNumber with 1999 (so number = 1999),
    # then at this point write would be 19, and "left" would be 99.
    # The laziest thing to do at this point is to make englishNumber
    # write out the 'nineteen' for us, then we write out ' hundred',
    # and then the rest of englishNumber writes out 'ninety-nine'.

    if left > 0
      # So we don't write 'two hundredfifty-one'...
      numString = numString + ' '
    end
  end

  write = left/10           # How many tens left to write out?
  left  = left - write*10   # Subtract off those tens.

  if write > 0
    if ((write == 1) and (left > 0))
      # Since we can't write "tenty-two" instead of "twelve",
      # we have to make a special exception for these.
      numString = numString + teenagers[left-1]
      # The "-1" is because teenagers[3] is 'fourteen', not 'thirteen'.

      # Since we took care of the digit in the ones place already,
      # we have nothing left to write.
      left = 0
    else
      numString = numString + tensPlace[write-1]
      # The "-1" is because tensPlace[3] is 'forty', not 'thirty'.
    end

    if left > 0
      # So we don't write 'sixtyfour'...
      numString = numString + '-'
    end
  end

  write = left  # How many ones left to write out?
  left  = 0     # Subtract off those ones.

  if write > 0
    numString = numString + onesPlace[write-1]
    # The "-1" is because onesPlace[3] is 'four', not 'three'.
  end

  # Now we just return "numString"...
  numString
end

puts englishNumber(  0)
puts englishNumber(  9)
puts englishNumber( 10)
puts englishNumber( 11)
puts englishNumber( 17)
puts englishNumber( 32)
puts englishNumber( 88)
puts englishNumber( 99)
puts englishNumber(100)
puts englishNumber(101)
puts englishNumber(234)
puts englishNumber(3211)
puts englishNumber(999999)
puts englishNumber(1000000000000)

Ahhhh.... That's much better. The program is fairly dense, which is why I put in so many comments. It even works for large numbers... though not quite as nicely as one might hope. For example, I think 'one trillion' would be a nicer return value for that last number, or even 'one million million' (though all three are correct). In fact, you can do that right now...

A Few Things to Try

  • Improve englishNumber. First, put in thousands. So it should return 'one thousand' instead of 'ten hundred' and 'ten thousand' instead of 'one hundred hundred'.
  • Expand upon englishNumber some more. Now put in millions, so you get 'one million' instead of 'one thousand thousand'. Then try adding billions and trillions. How high can you go?
  • "Ninety-nine bottles of beer..." Using englishNumber and your old program, write out the lyrics to this song the right way this time. Punish your computer: have it start at 9999. (Don't pick a number too large, though, because writing all that to the screen takes your computer quite a while. A hundred thousand bottles of beer takes some time; and if you pick a million, you'll be punishing yourself as well!)

Congratulations! At this point, you are truly a programmer! You have learned everything you need to write huge programs from scratch. If you have ideas for programs you would like to write for yourself, give them a shot!

Of course, building everything from scratch can be a slow process. Why spend time writing code that someone else has already written? You want to send some email? You want to save and load files on your computer? How about generating web pages for a tutorial where the code samples are actually run every time the page is loaded? :) Ruby has many different kinds of objects we can use to help us write programs better and faster.