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.
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!
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!
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
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.
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.
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:
a
as we initially defined it.
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.
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.
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:
add
two arguments: subtract(80, 10)
and multiply(subtract(20, 6), add(30, 5))
.
subtract
method call, returns 70
.
multiply
method call, furthermore has two arguments: subtract(20, 6)
and add(30, 5)
.
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
.
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.
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.
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!
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.
def greeting(name)
"Hello, " + name + ". How are you doing?"
end
puts greeting("Bob")
Video Walkthrough
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"
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
Write a program that includes a method called multiply
that takes two arguments and returns the product of the two numbers.
def multiply(number1, number2)
number1 * number2
end
puts multiply(4, 2)
Video Walkthrough
What will the following code print to the screen?
def scream(words)
words = words + "!!!!"
return
puts words
end
scream("Yippeee")
It will not print anything to the screen.
Video Walkthrough
1) Edit the method definition in exercise #4 so that it does print words on the screen. 2) What does it return now?
1. def scream(words)
words = words + "!!!!"
puts words
end
scream("Yippeee")
2. still returns nil
Video Walkthrough
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>'
You are calling a method called calculate_product
that requires two arguments, but you are only providing one.
Video Walkthrough