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. 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 arguments or not. Let's restructure our say method definition again so that we can assign a default parameter in case the calling code 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 an argument).

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). 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 Arguments

Sometimes, when calling a method, one or more arguments can be altered permanently; that is, we are mutating arguments.

We (Launch School) sometimes refer to mutating an argument as mutating the caller. This is technically incorrect since mutating the caller refers to a similar but distinct concept. We're gradually correcting these misuses of the term, but you're probably going to see us misuse this term from time to time, especially in older material.

Before diving further into mutating arguments, recall that we previously stated that method parameters 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.

One thing to keep in mind is that we can mutate objects. That's not the same thing as changing the variable, though it can be hard to see the difference. For instance, let's say we have a local variable a that stores an array. (We'll cover arrays in more depth later. For now, just think 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 mutates its argument permanently
def mutate(array)
  array.pop
end

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

If you haven't seen the #pop method before, check out the documentation.

Notice the difference between each print out? We have permanently modified the array that local variable a references by passing it to the mutate method, even though a is outside the method definition's scope. This is because the pop method mutates its calling object (the array specified by array). a is still pointing to the same array, but, in this case, the number of elements in the array has changed.

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

a = [1, 2, 3]

# Example of a method definition that does not mutate the argument
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 calling object, array, on line 5, so the array passed to no_mutate is not mutated.

How do you know which methods mutate arguments 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! We'll discuss this later in the Core Curriculum.

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 assigning the returned_value to the value returned by the add_three method. 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 that we're now using puts to output the incremented value, as opposed to implicitly returning it. Will the code 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

It looks like somewhere along the line, we got a nil and nil does not know how to respond to a times method call. Let's take things step by step and just run add_three(5). You should see something like this:

8                # This is the output
=> nil           # This is the return value!

Notice that it prints the incremented value as expected, but the return value is nil. It turns out that puts always returns nil. Since puts n + 3 is the last expression in the method definition, add_three returns nil . 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 return value or an exception is thrown, the entire chained call will break down. If we want the add_three method to print the incremented value as well as return it, 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.

Method Calls as Arguments

Thus 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.

The Call Stack

One important aspect of methods that all programmers need to understand is the concept of the call stack, or more casually, the stack.

The call stack helps Ruby keep track of what method is executing as well as where execution should resume when it returns. To do that, it works like a stack of books: if you have a stack of books, you can put a new book on the top or remove the topmost book from the stack. In much the same way, the call stack puts information about the current method on the top of the stack, then removes that information when the method returns.

In Ruby, methods aren't the only thing that uses the call stack. Blocks, procs, and lambdas also use the call stack; in fact, they all use the same call stack as Ruby uses for methods. For simplicity, we will usually just mention methods when discussing the call stack. However, you should always remember that blocks, procs, and lambdas are also part of this process.

Let's assume that we have the following code:

def first
  puts "first method"
end

def second
  first
  puts "second method"
end

second

When this program starts running, the call stack initially has one item -- called a stack frame -- that represents the global (top-level) portion of the program. The initial stack frame is sometimes called the main method. Ruby uses this frame to keep track of what part of the main program it is currently working on.

Call Stack
-
-
-
main

When program execution reaches the method invocation on line 10, it first updates the main stack frame with the current program location. Ruby will use this location later to determine where execution should resume when second finishes running.

After setting the location in the current stack frame, Ruby creates a new stack frame for the second method and places it on the top of the call stack: we say that the new frame is pushed onto the stack. Our call stack now looks like this:

Call Stack
-
-
second
main: line 10

Note that the frame for the second method is now stacked on top of the main frame. While the second frame is still on the stack, main remains stuck beneath it, inaccessible. At the same time, the main method becomes dormant and the second method becomes active.

The second method calls the first method on line 6. That action causes Ruby to update the second frame so that Ruby will know where to resume execution later. It then creates a new stack frame for the first method and pushes it to the call stack.

Call Stack
-
first
second: line 6
main: line 10

Once the first method begins executing, it invokes the puts method. All Ruby methods, including the built-in ones like puts, share the same call stack. Therefore, we need to record our current location and then push a new frame to the stack:

Call Stack
puts
first: line 2
second: line 6
main: line 10

Chances are, puts also has several internal method calls. However, we will ignore them and just assume that puts does all its work without any additional method calls. Instead, it just logs the message first method to the console, then immediately returns.

When puts returns, Ruby removes -- pops -- the top frame from the call stack. That's the frame for puts in this example. That leaves the previous stack frame exposed. Ruby uses this frame to determine where execution should resume. In this case, execution resumes immediately after line 2.

Call Stack
-
first: line 2
second: line 6
main: line 10

Eventually, the first method will return. When that happens, the first frame gets popped from the stack. That exposes the stack frame for second, and that, in turn, tells Ruby that it should resume execution on line 6.

Call Stack
-
-
second: line 6
main: line 10

Next, execution jumps to the puts call on line 7. Like before, the current location is recorded and a new frame is pushed to the stack:

Call Stack
-
puts
second: line 7
main: line 10

When this puts call returns, the stack frame gets popped and execution returns to second.

Call Stack
-
-
second: line 7
main: line 10

When second finishes executing, the stack frame for second gets popped from the call stack, exposing the stack frame for main. The main frame tells Ruby to resume execution on line 10.

Call Stack
-
-
-
main: line 10

Eventually, the main method has no more code to run. When this happens, the main frame gets popped from the stack, and the program ends.

Call Stack
-
-
-
-

The call stack has a limited size that varies based on the Ruby implementation. That size is usually sufficient for more than 10000 stack entries. If the stack runs out of room, you will see a SystemStackError exception.

If you need a slightly more visual approach, take 10 minutes to watch this video.

Please note that this video uses JavaScript, not Ruby. However, the JavaScript portions of the video are very basic, and shouldn't be too difficult to understand given your current Ruby knowledge. Most importantly, don't try to learn JavaScript at this point - that comes later.

The chief difference between the JavaScript shown in the video and what you know of Ruby is that JavaScript uses the term "function" in much the same way as Ruby uses the term "method". For our purposes, they represent the same concept. Syntax-wise, JavaScript uses the function keyword to define functions, while Ruby uses the def keyword to define methods.

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? That is, what value does each expression return?

    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.5.3/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