Methods

What Are Methods and Why Do We Need Them?

You'll often have a piece of code that needs to be executed many times in a program. Instead of writing that piece of code over and over, there's a feature in most programming languages called a procedure, which allows you to extract the common code to one place. In Ruby, we call it a method. Before we can use a method, we must first define it with the reserved word def. After the def we give our method a name. At the end of our method definition, we use the reserved word end to denote its completion. This is an example of a method definition named say:

def say
  # method body goes here
end

There's a comment in the method body to show you where the logic for the method definition will go. So why do we want a method named say? To say something, of course! Suppose we had the following code in a file named say.rb. Create this file and type these examples along.

puts "hello"
puts "hi"
puts "how are you"
puts "I'm fine"

Notice how we've duplicated the puts many times. We'd like to have one place where we can puts and send that one place the information we want to puts. Let's create a method definition to do that.

def say(words)
  puts words
end

say("hello")
say("hi")
say("how are you")
say("I'm fine")

On first glance this may seem silly, since we didn't save any lines of code, and in fact added more code. But what we've done is extracted the logic of printing out text, so that our program can have more flexibility.

We call (or invoke) the method by typing its name and passing in arguments. You'll notice that there's a (words) after say in the method definition. This is what's called a parameter. Parameters are used when you have data outside of a method definition's scope, but you need access to it within the method definition. If the method definition does not need access to any outside data, you do not need to define any parameters.

You will also see the term method invocation to refer to calling a method.

You can name parameters whatever you'd like, but like we said earlier, it is always the goal of a good programmer to give things meaningful and explicit names. We name the parameter words because the say method expects some words to be passed in so it knows what to say! Arguments are pieces of information that are sent to a method invocation to be modified or used to return a specific result. We "pass" arguments to a method when we call it. Here, we are using an argument to pass the word, or string of words, that we want to use in the say method definition. When we pass those words into the method definition, they're assigned to the local variable words and we can use them however we please from within the method definition. Note that the words local variable is scoped at the method definition level; that is, you cannot reference this local variable outside of the say method definition.

When we call say("hello"), we pass in the string "hello" as the argument in place for the words parameter. Then the code within the method definition is executed with the words local variable evaluated to "hello".

One of the benefits that methods give us is the ability to make changes in one place that affect many places in our program. Suppose we wanted to add a . at the end of every string we send to the say method. We only have to make that change in one place.

def say(words)
  puts words + '.'    ## <= We only make the change here!
end

say("hello")
say("hi")
say("how are you")
say("I'm fine")

Run this code using the ruby say.rb command from your terminal to see the result. We've now added a . on each line and we only had to add it once in our program. Now you're starting to see the power of methods.

Default Parameters

When you're defining methods you may want to structure your method definition so that it always works, whether given parameters or not. Let's restructure our say method definition again so that we can assign a default parameter in case the caller doesn't send any arguments.

def say(words='hello')
  puts words + '.'
end

say()
say("hi")
say("how are you")
say("I'm fine")

You'll notice that say() prints hello. to the console. We have provided a default parameter that is used whenever our method is called without any arguments. Nice!

Optional Parentheses

Many Rubyists will leave off parentheses when calling methods as a style choice. For example, say() could be rewritten as just say. With arguments, instead of say("hi"), it could just be say "hi". This leads to more fluid reading of code, but sometimes it can be confusing. Keep that in mind when you're reading Ruby; it can get tricky deciphering between local variables and method names!

Method Definition and Local Variable Scope

Before moving on to the next topic on methods, let's take a moment to discuss the concept of local variable scope within a method definition. A method definition creates its own scope outside the regular flow of execution. This is why local variables within a method definition cannot be referenced from outside of the method definition. It's also the reason why local variables within a method definition cannot access data outside of the method definition (unless the data is passed in as a parameter).

Let's practice this concept with the following example:

a = 5

def some_method
  a = 3
end

puts a

What's the value of a? Still 5, because method definitions create their own scope that's entirely outside of the execution flow.

Make sure you don't mix up method invocation with a block and method definition when you're working with local variable scope issues. They may look similar at first, but they are not the same. They have different behaviors when it comes to local variable scope.

# Method invocation with a block

[1, 2, 3].each do |num|
  puts num
end
# Method definition

def print_num(num)
  puts num
end

obj.method or method(obj)

There are two ways to call methods that we will discuss in this book. The some_method(obj) format is when you send arguments to a method call; in the previous example, obj is the argument being passed in to the some_method method. Sometimes, you will see methods called with an explicit caller, like this a_caller.some_method(obj). We will discuss this in more detail in part III on Object Oriented Programming. For now it's best to think of the previous code as some_method modifying a_caller. You'll have to memorize which way is required to call a method for now.

Mutating the Caller

Sometimes, when calling a method, the argument can be altered permanently. We call this mutating the caller. Before getting into that, recall that we previously stated that method arguments are scoped at the method definition level, and are not available outside of the method definition. For example:

def some_method(number)
  number = 7 # this is implicitly returned by the method
end

a = 5
some_method(a)
puts a

In the above code, we passed in a to the some_method method. In some_method, the value of a is assigned to the local variable number, which is scoped at the method definition level. number is reassigned the value "7". Did this affect a's value? The answer is no, because number is scoped at the method definition level and a's value is unchanged. Therefore, we proved that method definitions cannot modify arguments passed in to them permanently.

Of course, there's an exception to this rule. The exception is when we perform some action on the argument that mutates the caller, we can in fact permanently alter variables outside the method definition's scope.

Let's say we have a local variable a that stores an array. We'll cover arrays in more depth later as well but for now just remember our earlier explanation of arrays as ordered lists. Type the following code into a file named mutate.rb and run it to see the result.

We use p instead of puts here. These two are very similar with only small differences to the way Ruby prints the output. You can try both to see why we chose to use p.

a = [1, 2, 3]

# Example of a method definition that modifies its argument permanently
def mutate(array)
  array.pop
end

p "Before mutate method: #{a}"
mutate(a)
p "After mutate method: #{a}"

Notice the difference between each print out? We have permanently modified the local variable a by passing it to the mutate method, even though a is outside the method definition's scope. This is because the pop method mutates the caller.

Let's contrast this with a method definition that does not mutate the caller but still returns the same value.

a = [1, 2, 3]

# Example of a method definition that does not mutate the caller
def no_mutate(array)
  array.last
end

p "Before no_mutate method: #{a}"
no_mutate(a)
p "After no_mutate method: #{a}"

You'll notice that we have the same output before and after the method invocation, so we know that a was not modified in any way. This is because the last method does not mutate the caller.

How do you know which methods mutate the caller and which ones don't? Unfortunately, you have to memorize it by looking at the documentation or through repetition.

If you have experience programming in other languages and are wondering if Ruby is a pass-by-value or pass-by-reference language, then you might be disappointed with the answer. In a way, Ruby is both!

puts vs return: The Sequel

Now that you know what a method is and how it works, we can discuss the difference between puts and return. You haven't really been properly introduced to return but that's because in Ruby, every method returns the evaluated result of the last line that is executed.

Let's use our mutate.rb file to demonstrate this.

a = [1, 2, 3]

def mutate(array)
  array.pop
end

p "Before mutate method: #{a}"
p mutate(a)
p "After mutate method: #{a}"

We're using the p method to print out the value of whatever the mutate method returns. Our output looks like this:

"Before mutate method: [1, 2, 3]"
3
"After mutate method: [1, 2]"

Here's what's happening:

  1. We print out a as we initially defined it.
  2. We print out the value returned by the mutate method.
  3. We print out the value of a after the mutate method.

The second line, where it's returning a "3", is probably confusing you a little bit. What's happening is that the method is returning the result of array.pop back to where it's being called from. pop is a method in the Array class that removes the last element of an array and returns it.

Before we wrap this up, let's look at return by itself so we can fully understand it. Let's create a file called return.rb to demonstrate. Remember to type these examples out and create the files, your fingers are learning without you knowing it! Let's go!

def add_three(number)
  number + 3
end

returned_value = add_three(4)
puts returned_value

Here we're saving the returned value of the add_three method invocation in a variable called returned_value. Then we print returned_value to the output to see what it has inside it. Your output should print 7 because that's what the method call returned.

Ruby methods ALWAYS return the evaluated result of the last line of the expression unless an explicit return comes before it.

If you wanted to explicitly return a value you can use the return keyword.

def add_three(number)
  return number + 3
end

returned_value = add_three(4)
puts returned_value

Your output should still be the same, right? What happens if we change this again? What will print to the screen, if we run the code below?

def add_three(number)
  return number + 3
  number + 4
end

returned_value = add_three(4)
puts returned_value

The program above should still output 7, the number you told it to return.

When you place a return in the middle of the add_three method definition, it just returns the evaluated result of number + 3, which is 7, without executing the next line.

One of the major points that you will want to take away from this section is that the return reserved word is not required in order to return something from a method. This is a feature of the Ruby language. For example, consider this method definition:

def just_assignment(number)
  foo = number + 3
end

The value of just_assignment(2) is going to be 5 because the assignment expression evaluates to 5, therefore that's what's returned.

That about covers methods. You are getting wiser and more confident with Ruby. We have a good feeling that you're probably starting to have a good time as well. Keep going! It only gets better from here.

Chaining Methods

Because we know for certain that every method call returns something, we can chain methods together, which gives us the ability to write extremely expressive and succinct code.

Suppose we create the following method definition:

def add_three(n)
  n + 3
end

The above method will return - not print out, but return - the value passed in incremented by 3. We can use it like this:

add_three(5)        # returns 8

Since the add_three method call returns a value, we can then keep calling methods on the returned value.

add_three(5).times { puts 'this should print 8 times'}

This means that we're calling the times method on the returned value of add_three(5), which is 8. Run the above in irb and you get:

this should print 8 times
this should print 8 times
this should print 8 times
this should print 8 times
this should print 8 times
this should print 8 times
this should print 8 times
this should print 8 times
=> 8

Note the last line. That means the entire expression add_three(5).times { puts 'this should print 8 times'} returned 8, which implies we can keep chaining method calls if we wanted to!

In Ruby, it's common to see methods being chained together to form elegant code. For example:

"hi there".length.to_s      # returns "8" - a String

This is because the String length method returns an integer, and we can call to_s on integers to convert them into strings.

Ok, back to our original add_three method definition. Let's make a small modification:

def add_three(n)
  puts n + 3
end

Notice we're now using puts to output the incremented value, as opposed to implicitly returning it. Will the below work:

add_three(5).times { puts "will this work?" }

If we run the code, we get this error:

NoMethodError: undefined method `times' for nil:NilClass

Looks like somewhere along the line, we got a nil and nils do not know how to respond to a times method call. Let's take it step by step and just run add_three(5). The output is:

8
=> nil

Notice now it prints out the incremented value as expected, but the return value is now nil. It turns out puts returns nil, and since puts n + 3 is the last expression in the method definition, nil is now being returned by the add_three method call. We can now no longer use add_three to keep chaining methods since it returns nil.

This is a very important aspect of chaining methods together: if anywhere along the chain, there's a nil or an exception is thrown, the entire chained call will break down. If we want the add_three method to print out the incremented value as well as return it, then we have to make this fix:

def add_three(n)
  new_value = n + 3
  puts new_value
  new_value
end

We could use return new_value as well, but since new_value is the last expression in the method definition, it's being implicitly returned.

Methods as Arguments

So far we've become familiar with how methods are called. Let's take some simple examples to go over this concept. We're going to define add and subtract methods and call them:

def add(a, b)
  a + b
end

def subtract(a, b)
  a - b
end

We've defined two methods add and subtract that take parameters a and b. We assume both are integer values. Recall that Ruby implicitly returns the last line of a method; since both method definitions here contain just one line each, we're letting Ruby do its magic by using implicit return. Note that we could have also used explicit return to be more specific.

Now let's call these methods by passing integer values:

add(20, 45)
=> 65
# returns 65

subtract(80, 10)
=> 70
# returns 70

What is less obvious is that Ruby actually allows us to pass a method call as an argument to other methods. Stated differently, we're saying we can pass add(20, 45) and subtract(80, 10) as arguments to another method.

Remember that these method calls return integer values which is what allows us to perform such an operation. In other words, the returned value is what is being passed as arguments. We'll illustrate by defining a multiply method:

def multiply(num1, num2)
  num1 * num2
end

Now, let's pass add(20, 45) and subtract(80, 10) as arguments to multiply:

multiply(add(20, 45), subtract(80, 10))
=> 4550
# returns 4550

Let's see a more complicated example:

add(subtract(80, 10), multiply(subtract(20, 6), add(30, 5)))
=> 560

Let's break down what this is doing:

  • First, we're passing add two arguments: subtract(80, 10) and multiply(subtract(20, 6), add(30, 5)).
  • The first argument, the subtract method call, returns 70.
  • The second argument, the multiply method call, furthermore has two arguments: subtract(20, 6) and add(30, 5).
    • Here, subtract(20, 6) returns 14 and add(30, 5) returns 35 thus the method call becomes multiply(14, 35). Evaluating multiply(14, 35) now returns 490.
  • Finally, putting together the return values of those two method calls, we have add(70, 490) which ultimately returns 560.

One very important thing to be aware of when using nested method calls is the use of parentheses to prevent any kind of confusion.

We've seen that method calls always return a value and we can pass that method call as an argument to another method call based on the returned value. Thus it's vital to know what our defined methods are returning, since in the final analysis, this is what is actually being passed as arguments to other method calls.

Summary

Methods are a major part of programming in Ruby. Knowing what a method is and what operations it is performing is crucial to your development as a Ruby programmer. You'll be using them constantly, in programs both big and small. Knowing the difference between puts and return will help you avoid a common pitfall that we see many beginners struggle with. Finally, knowing how and when to use method chaining will help you better read code and let you write more succinct code. But watch out for those nils. Let's get into some exercises and put this knowledge to use!

Exercises

  1. Write a program that prints a greeting message. This program should contain a method called greeting that takes a name as its parameter and returns a string.

    Solution

    def greeting(name)
      "Hello, " + name + ". How are you doing?"
    end
    
    puts greeting("Bob")
    

    Video Walkthrough

    Please register to play this video
  2. What do the following expressions evaluate to?

    1. x = 2
    
    2. puts x = 2
    
    3. p name = "Joe"
    
    4. four = "four"
    
    5. print something = "nothing"
    

    Solution

    1. x = 2    # => 2
    
    2. puts x = 2    # nil
    
    3. p name = "Joe"    # => "Joe"
    
    4. four = "four"    # => "four"
    
    5. print something = "nothing"    # => nil
    

    Video Walkthrough

    Please register to play this video
  3. Write a program that includes a method called multiply that takes two arguments and returns the product of the two numbers.

    Solution

    def multiply(number1, number2)
      number1 * number2
    end
    
    puts multiply(4, 2)
    

    Video Walkthrough

    Please register to play this video
  4. What will the following code print to the screen?

    def scream(words)
      words = words + "!!!!"
      return
      puts words
    end
    
    scream("Yippeee")
    

    Solution

    It will not print anything to the screen.

    Video Walkthrough

    Please register to play this video
  5. 1) Edit the method definition in exercise #4 so that it does print words on the screen. 2) What does it return now?

    Solution

    1. def scream(words)
          words = words + "!!!!"
          puts words
        end
    
        scream("Yippeee")
    
     2. still returns nil
    

    Video Walkthrough

    Please register to play this video
  6. What does the following error message tell you?

    ArgumentError: wrong number of arguments (1 for 2)
      from (irb):1:in `calculate_product'
      from (irb):4
      from /Users/username/.rvm/rubies/ruby-2.0.0-p353/bin/irb:12:in `<main>'
    

    Solution

    You are calling a method called calculate_product that requires two arguments, but you are only providing one.

    Video Walkthrough

    Please register to play this video