GSwR V: Methods to the Madness

This entry is part 5 of 6 in the series Getting Started with Ruby

Getting Started with Ruby

We’ve covered some of Ruby’s most important object types in the last three posts – Strings, Integers & Floats and collections such as Arrays, Ranges and Hashes. We’ve also looked at the methods that give these objects their functionality. In this post, we’re going to look at creating our own methods.

To define a method we use the def keyword, followed by the name of the method. The code for the method comes next before finishing with the end keyword. Let’s have a look at some examples in IRB:

def say_hello
  "Hello Ruby!"
end

This method is called say_hello and returns the string “Hello Ruby!”. The last line of a method in Ruby is its return value which is the value that is returned by the method when it is called. To call a method, simply enter its name:

say_hello
=> Hello Ruby!

Methods are useful. They can make your code easier to read (as long as you give your methods descriptive names) and mean that you don’t have to write repetitive blocks of code over and over again. Also, if you want some of the functionality to change, then you only need to update the method in one place. This is known as the DRY (Don’t Repeat Yourself) principle and it is important to keep in mind when programming.

Adding Parameters

Methods can be made more effective by including parameters. These are values that are provide for the method to use. For example we can improve the say_hello method by adding a ‘name’ parameter:

def say_hello(name)
  "Hello #{name}!"
end

Parameters are added to the method definition after the method name (the parentheses are optional but recommended). In the body of the function, the name of the parameter acts like a variable equal to the value that is passed to the method as an argument when it is called:

say_hello "Sinatra" # Again, parentheses are optional
=> "Hello Sinatra!"

In this case, the string “Sinatra” is provided as an argument and we use string interpolation to insert the arguument into the string that is returned by the method.

We can provide a default value for a parameter by putting it equal to the default value in the method definition:

def say_hello(name="Ruby")
  "Hello #{name}"
end

This means we can now call the say_hello method with or without arguments:

say_hello
=> "Hello Ruby!"

say_hello "DAZ"
=> "Hello DAZ!"

We can add more parameters, some with default arguments and some without (but the ones without must come first). Here is another funtion called greet that uses multiple parameters:

def greet(name,job,greeting="Hello")
  "#{greeting} #{name}, Your job as a #{job} sounds fun"
end

greet("Walt","Cook")
=> "Hello Walt, Your job as a Cook sounds fun"

greet("Jessie","Cook","Hey")
=> "Hey Jessie, Your job as a Cook sounds fun"

We can also create methods with an unspecified number of arguments by placing a ‘*’ in front of the last parameter in the method definition. Any number of arguments will then be stored as an array with the same name as the parameter. The following method will take any number of names as an argument and store them in an array called names:

def say_hello(*names)
  names.each { |name| puts "Hello #{name}!" }
end

say_hello("Walt","Skylar","Jessie")
Hello Walt!
Hello Skylar!
Hello Jessie!
 => ["Walt", "Sklar", "Jessie"]

say_hello "Sherlock", "John"
Hello Sherlock!
Hello John!
 => ["Sherlock", "John"]

say_hello "Heisenberg"
Hello Heisenberg!
 => ["Heisenberg"]

Notice that the return value of the method is the names array.

Another option is to use keyword arguments instead (although these are only available from Ruby version 2.0 onwards). These act by using a hash like syntax for the parameters, as demonstrated in an updated version of our greet method:

def greet(name="Ruby", job: "programming language", greeting: "Hello")
  "#{greeting} #{name}, Your job as a #{job} sounds fun"
end

The arguments can then be entered in any order using the keywords:

greet greeting: "Hi"
=> "Hi Ruby, Your job as a programming language sounds fun"

greet "Sherlock", job: "detective", greeting: "Greetings"
=> "Greetings Sherlock, Your job as a detective sounds fun"

greet "John", greeting: "Good Day", job: "doctor"
=> "Good Day John, Your job as a doctor sounds fun"

Notice that the order of the keyword arguments doesn’t matter and, if some of them are omitted then, the default value is used instead.

We can also add an extra parameter at the end with ** in front of it. This will collect any extra keyword arguments that are not specified in the method definition in a hash with the same name as the parameter:

def greet(name="Ruby", job: "programming language", greeting: "Hello", **options)
  "#{greeting} #{name}, Your job as a #{job} sounds fun. Here is some extra information about you: #{options}"
end

greet "Saul", job: "Colonel", human: false
=> "Hello Saul, Your job as a Colonel sounds fun. Here is some extra information about you: {:human=>false}"

greet "Kara", job: "Viper Pilot", callsign: "Starbuck", human: true
=> "Hello Kara, Your job as a Viper Pilot sounds fun. Here is some extra information about you: {:callsign=>\"Starbuck\", :human=>true}"

It’s also possible to add a block as a parameter to a method, by placing a & before its name. The block can then be accessed in the method definition by referring to it. This is useful if you want to run some specific code when a method is called. Here’s a basic example of a method called repeat which accepts a block of code as well as an argument telling it how many times to run that code:

def repeat(number=2, &block)
  number.times { block.call }
end

repeat(3) { puts "Ruby!" }
Ruby!
Ruby!
Ruby!
 => 3

The block is optional and there is a handy method called block_given? that allows us to check if the block is given when the method is called. Here is a method called roll_dice that takes the number of sides to the dice as an argument as well as a block that can be used to do something with the value rolled on the dice:

def roll_dice(sides=6,&block)
  if block_given?
    block.call(rand(1..sides))
  else
    rand(1..sides)
  end
end

If the method is called without a block, then it simple returns the number rolled on the dice:

roll_dice
=> 6

If we want to mimic a 20-sided dice then we can enter a sides argument:

roll_dice(20)
=> 13

Now say we want to roll the dice, but then double the result and then add 5. We can use a block to do this:

roll_dice { |result| 2 * result + 5 }

Another example of using a block might be if we want to return whether the result of rolling the dice was odd or even:

roll_dice do |result|
  if result.odd?
    "odd"
  else
    "even"
  end
end

Using blocks as parameters can make methods extremely flexible and powerful. The route handlers in Sinatra take blocks as arguments. The method definition for a GET request looks similar to this:

def get(route,options={},&block)
  ... code goes here
end

The route argument is a string that tells us the route to match. This is followed by a hash of options set to an empty hash by default. Last of all, the method takes a block, which is the code that we want to run when the route is visited.

Refactoring Play Your Cards Right

We’re going to have a go at refactoring the code for the ‘Play Your Cards Right’ Sinatra app that we created in the last tutorial. Refactoring code is the process of improving its structure and maintainability without changing its behaviour.

What we’re going to do is replace some of the chunks of code with methods. This will make the code easier to follow and easier to maintain – if we want to make a change with the functionality, we just need to change the method in one place.

Sinatra uses helper methods to describe methods that are used in route handlers and views. These are placed in a helpers block that can go anywhere in the code, but is usually placed near the beginning and looks like this:

helpers do

  # helper methods go here

end

To start with, we’re going to rewrite the app using the names of methods to describe the behaviour we want. Creat a new file called ‘play_your_cards_right_refactored.rb’ and add the following code:

require 'sinatra'
enable :sessions

configure do
  set :deck, []
  suits = %w[ Hearts Diamonds Clubs Spades ]
  values = %w[ Ace 2 3 4 5 6 7 8 9 10 Jack Queen King ]
  suits.each do |suit|
    values.each do |value|
    settings.deck << "#{value} of #{suit}"
    end
  end
end

helpers do
 # helper methods will go here
end

get '/' do
  set_up_game
  redirect to('/play')
end

get '/:guess' do
  card = draw_card
  value = value_of card

  if player_has_a_losing value
    game_over card
  else
    update_session_with value
    ask_about card
  end
end

This is very similar to the last piece of code (and it has exactly the same functionality) except that it’s much easier to see what’s going on in each route handler. This is because we have replaced a lot of the Ruby code with methods and chosen some descriptive method names, making the code more readable. It almost looks like pseudocode.

Let’s have a look at each route handler in turn to see what it does:

get '/' do
  set_up_game
  redirect to('/play')
end

This route handler sets up the game then redirects to the ‘/play’ route … in fact, this shouldn’t even need explaining because it says it right there in the code! The method names tell us exactly what is happening – all of the code has been extracted into a method called set_up_game which needs to be created. Place the following inside the helpers block:

def set_up_game
  session[:deck] = settings.deck.shuffle
  session[:guesses] = -1
  session[:value] = 0
end

This sets up the session variables that we’ll need in the game. The deck is shuffled, the guesses are set to -1 (because it gets incremented by 1 on the first play, even though a correct guess hasn’t been made) and the value variable is set to 0.

redirect and to are both methods built into Sinatra and have been purposefully named so that the code in a route handler reads almost like English.

Now let’s have a look at the start of the route handler that deals with the gameplay:

get '/:guess' do
  card = draw_card
  value = value_of card

    # more code follows
end

First of all we need to draw a card, so we write a method to do that. Add the following in the helpers block:

def draw_card
  session[:deck].pop
end

This is a very short method, but it gives us more descriptive code.

Next we want to find out the value of the card. The code that we used for this in part 4 was quite a long case statement to check for picture cards. We can extract this into a method that takes a card as an argument and then returns the value of that card. The following code also goes in the helpers block:

def value_of card
  case card[0]
    when "J" then 11
    when "Q" then 12
    when "K" then 13
    else card.to_i
  end
end

Next, we have an if statement to check if the player has guess correctly or not:

if player_has_a_losing value
  game_over card
else
  update_session_with value
  ask_about card
end

This also uses the name of the methods to make the code very descriptive. The first method is called player_has_a_losing and it has a parameter named value. This combination of name and parameter name makes it read nicely. This method also needs to go in the helpers block:

def player_has_a_losing value
  (value < session[:value] and params[:guess] == 'higher') or (value > session[:value] and params[:guess] == 'lower')
end

This returns true or false depending on whether the value entered as an arguement is higher or lower than the value stored in session[:value] (the previous card’s value) compared to the player’s guess, which is stored in the params hash with a key of :guess.

If the player_has_a_losing method returns false, then we move on to two more functions. The first is update_session_with, which takes an argument of value. This does exactly as it says, as well as updating the number of guesses by 1. It also goes in the helpers block:

def update_session_with value
  session[:value] = value
  session[:guesses]  += 1
end

The next method that needs to go in the helpers block is ask_about. This simply asks the player whether the card is higher or lower. It takes the current card as the argument:

def ask_about card
  "The card is the #{ card }. Do you think the next card will be <a href='/higher'>Higher</a> or <a href='/lower'>Lower</a>?"
end

That’s the last of all our helper methods. If you try running the code by typing ruby play_your_cards_right_refactored.rb into a terminal and then visit http://localhost:4567 you should see the same game as before. This is exactly what we want to happen when we refactor our code – no change on the outside, but more readable and maintainable code on the inside.

Scope

In some of the helper methods we just used, we had to supply either the card or value as a parameter to the methods. You might be wondering why we had to do this when card and value both existed already as variables. This is because a variable only exists inside a method if it has been created in the method or if it has been entered as an argument. This can be seen in the following example (check it in IRB):

name = "Walt"
job = "teacher"

def say_my_name
  name = "Heisenberg"
  job = "cook"
  puts "You're #{name} and you're a #{job}"
end

puts "You're #{name} and you're a #{job}"
=> You're Walt and you're a teacher

say_my_name
=> You're Heisenberg and you're a Cook

When puts is called outside the method name is ‘Walt’ and job is ‘teacher’, but as soon as you go inside the method name becomes ‘Heisenberg’ and job is ‘cook’.

A method doesn’t have any access to any variable created outside of it (they need to be entered as arguments to the method)· You also can’t access any variable created inside a method from outside of the method either – any values you want to access after the method has been called should be returned by the method. The places in the code where a variable is accessible is known as the variable’s scope.

That’s All Folks

Hopefully this tutorial has helped to introduce methods and shown how useful they can be in making your code more flexible, maintainable, reusable, and easier to read (as long as they are well named).

The methods we were writing in this tutorial were actually more like functions. As I mentioned in the very first post, Ruby is actually an object orientated language and the methods should be methods of objects. The methods we have been writing are actually all methods of the special main object (See this post by Pat for more in-depth info about this).

In the next post, we’ll be getting classy and looking at how Ruby’s class system works. We’ll go over how to add methods to existing classes such as Strings and Integers and how to create your own classes with their own public and private methods. In the meantime, please leave any comments or questions that you might have in the comments section below.

Getting Started with Ruby

<< GSwR IV: Going Loopy Over Arrays and HashesGSwR VI: Stay Classy with Ruby >>

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • Matt

    You forgot the game_over method, which (I believe) should be:

    def game_over card

    “Game Over! The card was the #{ card }. You managed to make #{session[:guesses]} correct guess#{‘es’ unless session[:guesses] == 1}. Play Again

    end