4. Ruby Methods

Overview

By the end of this chapter, you will be able to define and call your own methods for Ruby programs; set parameters for methods in Ruby; provide default values for a parameter in Ruby; implement methods in Ruby that return multiple values and write code using the built-in modules of Time and Math.

Introduction

In the previous chapter, we learned about Boolean variables, conditional expressions, and loops, including the core Ruby concepts of using blocks. We will now zoom out a little in the Ruby world and learn about methods. In fact, we've been using methods in the previous chapters, and we will look at them in more depth now.

Methods are foundational units in Ruby. They allow you to wrap code in chunks that can be called with different parameters to get different outputs. Methods commonly have descriptive names and, as such, make clear what the underlying bundle of code does. For instance, we previously learned about the each method on arrays. This method enumerates over each item in the array, and so is very descriptive. In fact, method naming is an art where you balance simplicity with descriptiveness. It is considered that the difficult things in computer science are caching, naming things, and off-by-one errors. Commonly known as OBOB errors, off-by-one bugs are logical errors that occur when there are too many iterations in a program or when there are mistakes in the comparisons in code.

Methods are like atoms. They have can have internal parts; however, for the most part, they are fundamental units of code with a defined interface. In addition to this, methods are the building blocks for classes, which are the higher-order building blocks for Ruby programs.

Methods should be designed so that they are simple and can accomplish a basic purpose. If a method gets too long or complicated, then that is a good sign that you need to break up the method into smaller, more clearly labeled methods. By having clearly labeled single-purpose methods, readability, maintainability, and testability are increased. These three attributes are the cornerstones of great code.

Additionally, by defining a clear interface to the method, we can change the implementation at any time without fear of breaking any code that calls the method. In Ruby, this can be a bit trickier because Ruby is a dynamically typed language. This means that the arguments passed to a method or the values returned from a method can be of any type. For instance, a method can accept a string or an integer or any other type of object in its arguments. It's up to the method's implementation to handle each of these cases. A common approach to handling all these cases is actually not to worry about the specific types of arguments, but to worry about whether the arguments behave as we need them to instead. This is called duck typing and will be covered in this chapter.

The Basic Structure of the Ruby Method

As we have seen, the basic structure of a method is as follows:

def echo(var)

  puts var

end

We are defining a method named echo that accepts the var variable as a parameter or argument. The end keyword marks the end of the block of code. The code between the def and end statements is known as the method body or the method implementation.

Methods are always called on an object. In Ruby, a method is called a message, and the object is called the receiver. The online Ruby documentation uses this language specifically, so it is good to get used to this vocabulary.

In the previous example, however, it doesn't seem like there is an object. Let's investigate this through IRB:

def echo(var)

  puts var

end

=> :echo

echo "helloooo!"

helloooo!

=> nil

self

=> main

self.class

=> Object

Here, we've defined and called our echo method in IRB. We use the self keyword, which is a reference to the current object. In IRB, when we call self, we can see that the current object is main. We can call class on any object to find out what type of object it is. Here, the main object is simply a type of the Ruby Object class.

In Ruby, everything is an object; even classes are objects. This can be a bit confusing and we will discuss more about classes in the next chapter, Chapter 5, Object-Oriented Programming with Ruby. For now, just know that we can call methods on objects. Or, in Ruby parlance, we can send messages to receivers, or an object can receive a message.

In Ruby, the . (dot) notation is the syntax for how we send a message to a receiver.

When you call a method without a dot notation, there is an implicit lookup to find the method. This implicit lookup is the same as calling self.method.

Consider the following line of code:

irb(main):013:0> self.echo("test 1-2, is there anyone in here?")

The output should be as follows:

Figure 4.1: The self method output

Figure 4.1: The self method output

Here, we can see that calling echo on self gives us the same behavior.

Method Components: Signature and Implementation

A method is composed of two parts: its signature and its implementation. A method signature is basically the first line of a method, which defines the following:

  • How the method is called (as a class, instance, module, or anonymously)
  • Name of the method
  • Arguments (also known as parameters)

A method implementation is simply the body of the method after the signature (that is, everything after the first line of the method).

The Method Signature

A method signature is a combination of how the method is defined, its name, and its arguments. A method signature defines everything that is unique about the method and tells us everything we need to know in order to call the method. In Object-Oriented Programming (OOP), in order to utilize methods from other libraries or sections of code, we learn their signatures. As long as the signature of a method remains the same, a developer can feel free to reimplement a method however they wish, and we can count on our code not breaking from this change. This is called encapsulation and is a core concept in OOP. We will talk more about method signatures throughout this chapter, while we will talk more about encapsulation in the next chapter, Chapter 5, Object-Oriented Programming with Ruby.

There is a nuance about method signatures, implementations, and encapsulations to be aware of. Ruby is not statically typed, which means a method could be reimplemented such that the return value could change. The same method could be reimplemented where it previously returned a number and now returns a string. Ruby does not enforce return types; so, as a method developer, it is your responsibility to manage what types of values are being returned and to keep things consistent.

Here are some quick examples from the Ruby library:

Dir.entries("./")

The output should be as follows:

Figure 4.2: Directory entries

Figure 4.2: Directory entries

The preceding example calls the entries method on the Dir class; it accepts a single String argument and it returns an array of the entries in the current directory. Contrast this with an instance method on Array:

[1,2,3].length

The output should be as follows:

Figure 4.3: The length method of an array

Figure 4.3: The length method of an array

Here, we call the length method on an actual instance of an array; it does not take any arguments and returns an integer.

Note

To learn more about method signatures, refer to https://packt.live/2p3kw9a.

Method Arguments

As we discovered in the previous section, a method signature is one of the most important parts of a method, defining how it's called, what its name is, and what arguments it can take. Let's dive deeper into method arguments now.

Ruby methods can take any number of arguments. There are also different approaches to specifying arguments, with pros and cons to each. These approaches to specifying arguments are known as the method signatures.

There are different types of arguments available for Ruby methods:

  • Positional arguments
  • Optional parentheses
  • Mandatory and optional arguments
  • Keyword arguments

Let's take a look at each of these argument types one by one.

Positional Arguments

Positional arguments are arguments that are listed in the method signature and rely on their ordering in the signature. In other words, if a method signature has multiple arguments listed in a particular order, when the method is called with those arguments, Ruby will assign the arguments passed into the variables in the signature in the exact same order as they are specified.

Consider the following example:

def adjust_thermostat(temperature, fan_speed)

  puts "Temperature is now: #{temperature}"

  puts "Fan speed is now: #{fan_speed}"

end

adjust_thermostat(50, :low)

adjust_thermostat(:low, 50)

Here, we have a method with two positional arguments. In the first example, we call the method correctly by passing the temperature first and the fan speed second. However, in the second call, the parameters are called in the incorrect order and could have unintended consequences. The output should be as follows:

Figure 4.4: Output for positional arguments

Figure 4.4: Output for positional arguments

In this case, we are just outputting the values, so there is no issue. However, if we were doing any kind of calculation on the temperature, we could encounter an error when doing a mathematical operation on a symbol.

Variable Scope

Before we move on to discussing the other types of arguments, let's first talk about variable scope. Variable scope can be defined as the parts of code that are able to have access to certain variables. Variable scope is a much larger topic that is discussed throughout this book as we talk about the different fundamentals of Ruby code. For the purposes of methods and arguments, we need to know that method arguments are local to the method implementation.

In other words, these variables are not available outside the scope of the method block. Here is an example:

def adjust_temperature(temperature, adjustment)

  current_temperature += adjustment

  return current_temperature

end

puts adjust_temperature(50,5)

puts current_temperature

puts adjust_temperature(50,-5)

puts current_temperature

The output should be as follows:

Figure 4.5: Variable scope arguments

Figure 4.5: Variable scope arguments

Here, we define a variable called current_temperature outside the method block. We have a variable with the same name inside the method block. However, they are really two different variables, as we can see in the preceding example: the current_temperature variable outside the block never gets modified despite being modified inside the method block.

That said, there is a nuance here as regards objects that are passed as arguments. For instance, if you pass a hash as an argument and modify it in the method, it will be modified outside the method. Consider the following example:

  def adjust_temperature(climate_options, desired_temperature)

    climate_options[:temperature] = desired_temperature

  return nil

end

climate_options = {temperature: 50, fan_speed: :low}

adjust_temperature(climate_options, 55)

climate_options

The output would be as follows:

Figure 4.6: Modifying the hash inside a method

Figure 4.6: Modifying the hash inside a method

In this case, we modified the hash that was passed as an argument inside the method block. This is because the hash is passed to the method in a particular way, called pass by reference. This is in contrast to the previous example, which is pass by value.

Note

For further reading, search for "pass by value" and "pass by reference" in Google to learn more about variable scope and passing arguments to methods.

Understanding variable scope is important to consider as each new concept of Ruby is learned. Variable scope will depend on the context. We will learn more about variable scope in the next chapter, Chapter 5, Object-Oriented Programming with Ruby.

Optional Parentheses and Code Style

In Ruby, as we have already seen, we have defined method signatures as having a name, followed by parentheses, and then the arguments within the parentheses. However, it is optional to include parentheses in the method signature:

def hello! # no parameters, no parentheses, no worries

  puts "hello!"

end

def hello() # valid syntax, bad style

  puts "hello!"

end

def echo age1, age2 # valid syntax, bad style.

  puts age1, age2

end

In any programming language, there are many ways to write code, and this gives rise to "coding style." In Ruby, there is a generally accepted style guide, which can be found here: https://packt.live/2Vr542N. The Ruby style guide advises us to not use parentheses when there are no parameters and to always use parentheses when there are parameters.

Mandatory and Optional Arguments

So far, we have learned how to call methods with arguments. Let's see what happens when we try to call a method without passing in an argument:

ArgumentError (wrong number of arguments (given 0, expected 2))

The output should be as follows:

Figure 4.7: Calling a method without an argument

Figure 4.7: Calling a method without an argument

By declaring the variables as we have done so far in the preceding method signatures, we are making them mandatory arguments.

We can also declare variables in a method signature to be optional. By making variables optional, we have to supply a default value for them:

def log(message, time = Time.now)

  return "[#{time}] #{message}"

end

Here, we've defined a method to return a string to be entered into a log file. It accepts the log message and an optional argument of time. In order for a variable to be optional, it has to have a default value. In this case, the current time is the default value.

Note

The default value in the method signature is always executed at runtime. Also, the default value can be any value, including nil. Just make sure that you handle those possible values in your method implementation.

The optional arguments must always be listed last and cannot be in the middle of mandatory arguments.

Keyword Arguments

Ruby 2.0 introduced keyword arguments. Keyword arguments, in contrast to positional arguments, allow arguments to be specified in the method signature by keyword instead of by their position in the signature.

Consider a method to process a payment by subtracting the price of an item from a balance. Prior to Ruby 2.0, the code to implement such a method would have been the following:

def process_payment(options = {})

  price = options[:price]

  balance = options[:balance]

  return balance - price

end

process_payment(price: 5, balance: 20)

The balance and price of the item are passed in a single hash argument. The default options argument is replaced by the argument that is passed in during the method call.

However, with Ruby 2.0, named arguments allow us to refactor the method as follows:

def process_payment(price:, balance: )

  return balance - price

end

process_payment(price: 5, balance: 20)

The invocation is the same, but the implementation of the method is much cleaner because the arguments are named ahead of time. We don't need to extract them from a hash. The other advantage here is that keyword arguments can be listed in any order in the method signature or the method invocation. Contrast this with the positional arguments that we learned about in the first section of this chapter.

This advantage allows us to add or remove arguments from the method signature without having to worry about other code that may reference this method. This is a huge advantage.

You can, of course, use default options with named arguments just as with positional arguments:

def log_message(message: ,time: Time.now)

  return "[#{time}] #{message}"

end

log_message(message: "This is the message to log")

Sometimes, named arguments can be more verbose than using positional arguments. So, the choice to use one or the other is up to the developer and the use case.

Note

Positional arguments are easier, more lightweight, and less verbose. Keyword arguments are more explicit and verbose, but allow the reordering of parameters.

Return Values

Methods are great for wrapping up blocks of code that accomplish some purpose. In many cases, we want to capture the results of that code execution in one or more variables. As such, the method can return these variables to the caller of the method. Conveniently enough, the keyword for returning variables is return.

Here is a basic case of adding two numbers:

def sum(var1, var2)

  return var1 + var2

end

In Ruby, the value of the last line of code in a method is implicitly returned, so we also write this as follows:

def sum(var1, var2)

  var1 + var2

end

total = sum(1, 3)

The choice to do one or the other depends on the developer's style and the requirements of the program.

The Ruby style guide says to avoid the use of the return keyword when not required for flow control.

Multiple Return Values

Ruby methods can also return multiple values. Let's take a look at some simple examples.

The following method returns a pair of random numbers:

def get_random_pair

  return rand(10), rand(10)

end

There are a few ways to capture these return values:

pair = get_random_pair

  item1, item2 = get_random_pair

As we can see, Ruby is smart enough to figure out what type of assignment we are trying to do. Ruby knows we have an array of two and we are trying to assign values to two variables. Let's see what happens when there is not a 1:1 ratio of return values to variable assignments:

def get_three_items

  return rand(10), rand(10), rand(10)

end

item1, item2 = get_three_items

We can see here that Ruby put a single value for the item1 and item2 variables. The third item was discarded. If you run this code, you will see something similar to the following:

Figure 4.8: Multiple return values

Figure 4.8: Multiple return values

IRB is outputting an array of three items. The method returns the three items no matter what, but Ruby can only assign them to two variables. However, the entire line of code still has a separate return value. We can demonstrate this with the following:

overall_return = (item1, item2 = get_three_items)

The output should be as follows:

Figure 4.9: The overall return value

Figure 4.9: The overall return value

This demonstrates that we can do variable assignment from the return value(s) of a method and still have an overall_return value from the whole statement. In most cases, the overall_return value is the same as the individual variable assignment; however, this demonstrates that it doesn't have to be, especially when you have multiple return values.

Also, as we can see, the overall_return value is an array, even though we listed the return values as a list of variables. This is because Ruby implements any set of multiple values as an array. Convenient, right? This means that we can also return an array and do the same thing with variable assignment and overall_return values.

We can list the return values in an array very easily using the Array#last method. Consider a basic array with five items:

array = [1,2,3,4,5]

We can use the following code to obtain the last two items from the array and assign them to a single variable:

top2 = array.last(2)

The output should be as follows:

Figure 4.10: Output for the array.last method

Figure 4.10: Output for the array.last method

Use the following code to assign variables individually:

var1, var2 = array.last(2)

The output should be as follows:

Figure 4.11: Individual variable assignment

Figure 4.11: Individual variable assignment

We can use the following code to assign variables individually, discarding the last item:

var1, var2 = array.last(3)

The output should be as follows:

Figure 4.12: Output after discarding the last item from an array

Figure 4.12: Output after discarding the last item from an array

Until now, we have discussed the components of a method and how to implement them in Ruby. We have looked at various types of arguments and learned how to pass these arguments to methods. We also discussed how the return keyword can be used to capture the results from a method's execution in a variable. Now we will implement these concepts in an exercise.

Exercise 4.01: Performing Operations on an Array

In this exercise, we will be creating a method that returns the sum, mean, and median values of an array.

The following steps will help you to complete the exercise:

  1. Create a new Ruby file. Define the method and the return variables:

    def array_stats(input_array)

      return sum, median, mean

    end

  2. Then, we define a method that will calculate the sum value using the inject method. The inject method is used to sequentially add values, one by one, from the array:

    def array_stats(input_array)

      sum = input_array.inject(0){|total, i| total + i }

        return sum

    end

  3. We modify the method that will calculate the mean value:

    def array_stats(input_array)

      sum = input_array.inject(0){|total, i| total + i }

      mean = sum/input_array.length

        return sum, mean

    end

  4. Calculate median by creating another method that is dedicated to that purpose.

    Note

    The median is essentially the middle number in a sorted set of numbers. It is the value that separates the top half from the bottom half. As such, we need to write a method that calculates this. The median or middle number will depend on whether the set of numbers has an odd number or even number of elements.

    Hence, we have used an if/else block in the method to address these two scenarios:

    def calculate_median(array)

      array = array.sort

      if array.length.odd?

        array[(array.length - 1) / 2]

      else array.length.even?

        (array[array.length/2] + array[array.length/2 - 1])/2.to_f

      end

    end

  5. Bring all the code together for sum, mean, and median:

    def array_stats(input_array)

      sum = input_array.inject(0){|total, i| total + i }

      mean = sum/input_array.length

      median = calculate_median(input_array)

        return sum, mean, median

    end

  6. Call sum, mean and median with different input arrays and capture the values into variables:

    array_stats([1,2,3])

    stats = array_stats([500, 12, 1, 99, 55, 12, 12])

    sum, mean, median = array_stats([500, 12, 1, 99, 55, 12, 12])

    The output should be as follows:

    Figure 4.13: Output for sum, mean, and median

Figure 4.13: Output for sum, mean, and median

Note

The order of return values is important because they are similar to positional arguments. The order of how you list them will determine how they get assigned.

The Splat Operator

Suppose you want a method to have arguments and you want to accept a varying number of them. Ruby has a special syntax to account for this case. It is called the splat operator and is denoted as *. Actually, this is called the single splat operator. Since Ruby 2.0, there is also a double splat operator, **. Let's take a look at both of these.

The Single Splat (*) Operator

The single splat operator, *, is actually a way to work with arrays. Remember from earlier that Ruby really implements arguments as arrays behind the scenes? The splat operator allows us to build arrays together into a single variable or to splat them out into multiple variables. One of the main uses of the splat operator is to use it as a catch-all method argument. In other words, you can pass multiple arguments to a method call, and if the method signature defines an argument with the splat operator, it will combine all those arguments into the one variable.

Consider the following example:

def method_with_any_number_of_args(*args)

  puts args.class

  puts args

end

In this method signature, we have a single argument called args, which is denoted by the splat operator. This tells Ruby that the method can be called with any number of arguments and to splat them into a variable as an array.

It's not very common to use this form of method signature. Using this form requires more code to parse the array for specific arguments and can lend the code to be more error-prone and hard to debug. It's also not clear by reading the method signature what types of arguments the method can handle.

That said, knowing the splat operator exists and how it works can come in handy. We'll learn in the final chapter about a special Ruby method called method_missing, which uses the splat operator. In brief, this method is called by Ruby on an object when the object receives a message (method) that is not defined.

Let's take a look at some uses of the splat operator:

method_with_any_number_of_args(5, "asdf", [1,2,3])

Here, we pass three arguments of varying types and all arguments are put into the args variable as an array that can be further processed.

A useful usage of the splat operator is to assemble arguments as an array, and then to splat them out as arguments:

args = []

args << 5

args << "asdf"

args << [1,2,3]

method_with_any_number_of_args(*args)

We can also use the splat operator to send our assembled arguments, even if the method signature has traditional positional arguments:

def foo(arg1, arg2)

  puts arg1, arg2

end

args = [1,2]

foo(*args)

The Double Splat (**) Operator

The double splat operator arrived with Ruby 2.0 and pairs with its keyword arguments. Whereas the single splat operator can be used for splatting positional arguments into a single argument, the double splat operator is used for splatting keyword arguments into a single argument.

Let's take look at a method with keyword arguments:

def log_message(message:, time: Time.now)

  return "[#{time}] #{message}"

end

log_message(message: "This is the message to log")

The output should be as follows:

Figure 4.14: Output with keyword arguments

Figure 4.14: Output with keyword arguments

Now let's implement the same method with the double splat operator:

def log_message(**params)

  puts params.inspect

end

log_message(message: "This is the message to log", time: Time.now)

The output should be as follows:

Figure 4.15: Using the double splat operator

Figure 4.15: Using the double splat operator

Let's perform an exercise in which we will implement the splat operator.

Exercise 4.02: Creating a Method to Take Any Number of Parameters

In this exercise, we will write a method that emulates an API client that can take any number of parameters. Additionally, we will call the method using the single splat operator.

A common idiom is to assemble arguments in this way if there are conditions determining which arguments to pass to the method. Let's take a look at an example using an API client that makes a web request. The web request method can take any number of arguments:

  1. Open the IDE and create a new file. First, we need to create a method that implements a method called get_data and takes the url, headers, and params variables, which use the splat operator:

    def get_data(url, headers = {}, *params)

      puts "Calling #{url}"

      if headers.length > 0

      puts "Headers: #{headers}"

      else

      puts "No headers"

    end

      params.each do |param|

        puts "Found param: #{param}"

      end

    end

  2. Write a method that assembles parameters into an array:

    def assemble_params(include_headers = false, include_date_in_search = false, only_show_my_records = true)

      headers = {accept: "application/json"}

      url = "https://exampleapi.com"

      args = [url]

      args << headers if include_headers

      params = []

      params << "date=#{Time.now}" if include_date_in_search

      params << "myrecords=true" if only_show_my_records

      args << params if params.length > 0

    end

  3. Call methods with different values using the splat operator:

    get_data(*assemble_params)

    get_data(*assemble_params(true))

    get_data(*assemble_params(false, true, false))

    The output should be as follows:

    Figure 4.16: Output of the method taking multiple parameters

Figure 4.16: Output of the method taking multiple parameters

We have written a method that emulates an API client that can take any number of parameters.

Duck Typing

Ruby is a dynamically typed language. Other languages are statically typed. The difference comes down to whether, and how, the language enforces the consistency of the type of variables. The consistency can be enforced (or not, as in Ruby) on the method arguments.

For example, a method can have a single argument, and there is nothing that restricts the code from calling that method and passing a variable of any data type. You can pass strings, arrays, and integers into any argument you want and Ruby will not complain. However, the method implementation may complain if you pass it a type that it cannot handle.

While this behavior leads to very flexible code, it can also lead to vulnerable code or code that needs to accommodate many different types. There is a split among developers, with some who prefer dynamically typed languages over statically typed languages.

As Ruby enthusiasts, we love dynamic typing because of its flexibility and expressiveness. In statically typed languages, you often have to declare the type ahead of time. However, you may not know or care about the type. If the variable passed into a method can be anything, then that would mean that we need to handle lots of different cases in code.

As long as the variable passed in behaves as you need it to, it doesn't matter what type is passed in. That is what is termed as duck typing.

For instance, say you implement a logging method that outputs the length of an object passed into it, as in the following example:

def log_items(myvar)

  if myvar.kind_of?(Array) || myvar.kind_of?(Hash) || myvar.kind_of?(String)

    puts "Logging item with length: #{myvar.length}"

  end

end

In the preceding method, we are only logging items that respond to the length method. But what if there is a new type of object that also responds to length? Well, we would then have to add to the already quite long if conditional.

Instead, we can duck type the method so that it checks whether the variable responds to the method we need, in this case, length, as shown in the following code:

def log_items(myvar)

  raise "Unhandled type" unless myvar.respond_to?(:length)

  puts "Logging item with length: #{myvar.length}"

end

That is a lot cleaner and allows any object to be passed into the method and it will succeed as long as the object responds to the length method. respond_to? is a special Ruby method on all objects that allows code to check whether the method is defined or, rather, can respond_to the method in question, which is passed in as a symbol.

Duck typing gets its name from the old adage, "If it looks like a duck and it walks like a duck, then it is a duck." In other words, if an object has the characteristics that the method needs, we don't care about its type.

Sending a Message

So far, we've seen the standard way to send a message to Ruby objects using the dot (.) notation. Another way to send a message to a Ruby object is through the send method, as shown in the following code:

array = [1,2,3]

array.length # standard way of sending a message to an object

array.send(:length) # using the send method

array.send("length") # You can send a symbol or a string

send also supports passing arguments to methods:

array.send(:last, 2)

send will pass the positional arguments to the method in the same order that the method defines them. In other words, the second argument to send is really the first argument to the method that is actually being called. The third argument to send is the second argument to the method being called, and so on.

Let's take a look at some additional cases.

In the following example, we send the each method and can still pass a block:

[1,2,3].send(:each) do |i|

puts i

end

Let's take a look at a method that has keyword arguments:

irb(main):068:0> def foo(arg1: , arg2: )

irb(main):069:1> puts arg1, arg2

irb(main):070:1> end

=> :foo

irb(main):071:0> send(:foo, 1, 2)

Traceback (most recent call last):

  3: from /Users/peter/.rbenv/versions/2.5.1/bin/irb:11:in '<main>'

  2: from (irb):71

  1: from (irb):68:in 'foo'

ArgumentError (wrong number of arguments (given 2, expected 0; required keywords: arg1, arg2))

We tried to call send with positional arguments, but Ruby is telling us we need to call with keyword arguments, and even tells us the names. So, let's try again:

send(:foo, arg1: 1, arg2: 2)

1

2

=> nil

send isn't used too often and is used in advanced Ruby programming. However, it's good to be introduced to it as it does have some interesting properties. One reason you might want to use send is if you don't know the method ahead of time. For instance, the actual method called might depend on some conditions, be assembled dynamically, or you might receive the method from user input.

The following is an interesting example of using send to create a method that can call any method passed in from user input:

def call_anything(object, method)

  object.send(method)

end

call_anything([1,2,3], gets.chomp.to_sym) # try it with sum

call_anything({a: 1, b: 2},gets.chomp.to_sym)

Additionally, we will learn, in Chapter 5, Object-Oriented Programming with Ruby, how send can bypass method visibility when using OOP in Ruby.

Note

Passing user input to send is inadvisable because, if you pass a variable that comes from user input to send, it could lead to a security vulnerability for remote code execution. It also leads to unclear and unpredictable code.

Using Built-In Modules: Time and Math

The Ruby language is composed of both a core set of libraries and a standard set. The Ruby Core library contains libraries that are always available in any Ruby interpreter. These libraries cover the basic programming constructs such as working with files, objects, arrays, hashes, and other primitives. The Ruby Standard Library contains additional libraries that you will find useful as a developer but are not considered core to the language. Examples of these include temp files, web servers (WEBrick), matrices, networking libraries (net/http or net/ftp), and more.

As you learn Ruby, you should take the time to see which libraries are included in Core and which are included in the Ruby Standard Library. You will use these libraries frequently. For instance, you could implement your own library to parse URLs, but the Ruby development team knows this is a very common task and has provided the URI module in the standard library. In your humble author's opinion, the best developer is not the one who knows how to write low-level routines, but the one who has the best fluency in the libraries at their disposal. In this section, we will cover how to use two libraries from the Core library: Math and Time.

While Math and Time are both libraries in the Ruby Core library, they are different types of libraries. Math is a module and Time is a class. We will elaborate more deeply on classes and modules in the next two chapters. For now, all you need to know is that they exist and that methods are called in a slightly differently way.

Math

The Math library is a library in the Ruby core set of libraries that provides methods for many mathematical functions. In this section, we will learn how to see what's available in this library.

Let's solve an exercise to acquaint ourselves better with the library.

Exercise 4.03: Using the Math Library to Perform Mathematical Operations

In this exercise, we will be using the Math library from Ruby to perform basic mathematical operations:

  1. Open up an IRB console.
  2. Begin by typing the following command to identify the class:

    Math.class

    The output should be as follows:

    Figure 4.17: The Math module in Ruby

    Figure 4.17: The Math module in Ruby

    This confirms to us that Math is indeed a module. This fact, combined with the fact that our preceding list of methods is of "module methods" (prefixed with the scoping operator, ::), tells us that we can call these methods directly on Math.

  3. Perform the sine operation on an integer:

    Math.sin(45)

    The output should be as follows:

    Figure 4.18: The sine operation

    Figure 4.18: The sine operation

  4. Next, calculate the square root of an integer value:

    Math.sqrt(144)

    The output should be as follows:

    Figure 4.19: The square root function

    Figure 4.19: The square root function

  5. Next, build the numbers for a sine wave. Begin by creating a start value and an increment:

    x = -10

    increment = 0.25

  6. Now let's write a while loop to create a set of x-y pairs for the sine wave:

    while(x<10) do

    y = Math.sin(x)

    puts "#{x} #{y}"

    x += increment

    end

    In this example, our sine wave will go from -10 to 10. The while loop will simply output the x-y pair with a space in between. The output would look as follows:

    Figure 4.20: Output for x,y pairs

    Figure 4.20: Output for x,y pairs

    Here, we can see that the y value starts at 0.554 when the x value is -10, and that by an x value of -5.75, the y value has gone through being negative back to being around 0.5, which is characteristic of half of the wavelength of a sine wave.

  7. Next, let's visualize this is in a spreadsheet such as Google Docs or Microsoft Excel. The following instructions may vary depending on your spreadsheet software. First, select and copy all the x-y pairs into your clipboard. Next, paste this into your spreadsheet software. It should show up as follows:
    Figure 4.21: x-y pairs

    Figure 4.21: x-y pairs

  8. Next, we need to split our x-y pair into separate columns. The command will vary depending on your spreadsheet software.

    For Google Docs, enter =SPLIT(A1, " ") into cell B1.

    For Microsoft Excel, you can use the Text to Columns feature on the Data tab.

    Follow the wizard and make sure to pick Space as your delimiter:

Figure 4.22: Converting text into columns

Figure 4.22: Converting text into columns

You should end up with two columns of x-y value pairs. Next, highlight your two x-y pair columns, click on Insert > Chart, and then choose the line chart. Depending on your spreadsheet software, you may end up with two series (make sure to remove the straight-line series). You should end up with a nice-looking sine wave:

Figure 4.23: Sine-wave output

Figure 4.23: Sine-wave output

Thus, we have successfully used the Math module to perform mathematical operations and we have also generated a sine wave.

Time

The Time library is probably one of the most commonly used Ruby libraries. Almost every project you will work on will likely need to incorporate some concept of Time. As mentioned previously, Time is implemented as a class, not as a module.

This is actually something into which we will go deeper over the next two chapters. However, for now, we will cover the differences briefly. Consider that Math and mathematical functions never change. sine(45) will always have the same answer. However, time is fluid. Yesterday is different than today. 5 minutes ago is different than last year. Time, as a class, is used to represent a particular state of time. To do that, we have to create different instances of time. These are also known as objects. Let's investigate this by way of an exercise.

Exercise 4.04: Performing Method Operations with the Time Module

In this exercise, we will explore the built-in Time module of Ruby and perform various operations with it:

  1. Jump into an IRB session and play around with Time. First, we will check the type of the module:

    Time.class

    The output should be as follows:

    Figure 4.24: Time.class

    Figure 4.24: Time.class

    By calling Time.class, we see that Class is returned, which confirms that Time is indeed a class.

  2. Call one of those methods that were prefixed by the scoping operator, ::, straight on the Time class:

    Time.now

    The output should be as follows:

    Figure 4.25: Time.now

    Figure 4.25: Time.now

    We called Time.now and it returned some output, which seems like a string that represents the current time.

  3. Call the .class method on the value returned by Time.now:

    Time.now.class

    The output should be as follows:

    Figure 4.26: The .class method on time.now

    Figure 4.26: The .class method on time.now

    Here, we can see that Time.now is not a string after all but an object that is a Time class. This can be a little difficult to rationalize at this point before we have learned about classes, objects, and instances, but bear with us here. Time is a class (really, it's a class constant) and Time.now returns an instance of the Time class.

    Because we know that Time.now returns an instance of Time, we know that we can now call any of the instances on this object.

  4. Save Time.now to a variable. Additionally, try calling Time.now successively after that:

    t = Time.now

    The output should be as follows:

    Figure 4.27: Methods on time.now

    Figure 4.27: Methods on time.now

    By saving Time.now to a variable, we've saved that exact instance of time; that variable will always represent that instance. As long as we don't mutate that variable, that instance of time is stopped.

    Calling Time.now successively, though, shows that each call returns a new instance of time that represents the time at the exact instant it was called. We can see time advance with each successive call. That is what we would expect with the semantic call to Time.now.

    When we call the t variable, it's holding the original time that was saved when we called Time.now several seconds prior.

    Let's play with a few more methods.

  5. Call the hour, min, and day methods on time instance t:

    t.hour

    t.min

    t.min

    The output should be as follows:

    Figure 4.28: Methods on time instance t

    Figure 4.28: Methods on time instance t

    Here we are calling several methods on time instance t, which are returning different values depending on what we are asking: the hour, the minute, or the day of the month.

    We can go further with Time. The astute reader will have noticed that the first few instance methods on Time are the symbols +, -, and <=>.

    The + and symbols are the standard math operators for addition and subtraction. Let's explore this further by creating two Time instances and storing them in variables and then performing the addition and subtraction operations on them.

  6. Call Time.now at different intervals and calculate the difference:

    t1 = Time.now

    t2 = Time.now

    The output should be as follows:

    Figure 4.29: Difference in Time instances

    Figure 4.29: Difference in Time instances

    Here, there are two Time instances that are roughly 7 seconds apart. Add the instances and check the result:

    t1 + t2

    The output should be as follows:

    Figure 4.30: Sum of the two Time instances

    Figure 4.30: Sum of the two Time instances

    We get an error. We're not actually adding two Time instances together, but we're adding a number of seconds to a time. This makes more sense. Let's try it:

    t1 + 7

    The output should be as follows:

    Figure 4.31: Adding two Time instances

    Figure 4.31: Adding two Time instances

  7. When we subtract two Time instances, we get a floating-point number, and when we subtract a number from a Time instance, we get another time.

    t2 – t1

    The output should be as follows:

Figure 4.32: Subtracting two Time instances

Figure 4.32: Subtracting two Time instances

With this, we have successfully explored the Time module of Ruby.

Activity 4.01: Blackjack Card Game

Write a program that allows us to play the card game Blackjack. This version of Blackjack will be played with a single player and a computer dealer. The objective is for the player to get their cards as close to 21 as possible, without going over, and to beat the dealer's hand. Each player will be dealt a hand and the player will only be able to see one card from the dealer. The player can decide to hit or to stay, making a judgment based on what the dealer is showing and their own hand.

You can represent a single card as a number between 1 and 13. The four suits are diamonds, clubs, hearts, and spades:

  1. Write a method to generate a deck of cards.
  2. Write a method to shuffle the deck of cards.
  3. Write a method to identify the cards drawn in a hand.
  4. Write a method to label the cards based on their numbers and suits.
  5. Write a method that defines the card value and displays a hand of cards with its total.
  6. Write a method that determines the winner based on two hands (that is, two sets of cards). You will need a way to ask the user whether they want to hit or stay. Hitting means adding another card to the player's hand. Staying means keeping the current hand.

    Additionally, you will need a way for the dealer to determine whether it should hit or stay. The dealer could follow a fixed set of rules. If the total of the current hand is less than 17, it must hit. If the dealer's hand totals 17 or higher, it must stay.

  7. Write a loop that allows a player to continue playing as long as there are enough cards in the deck. The minimum number of cards for a hand is 4 (2 for the player and 2 for the dealer).

    Note

    In the middle of a hand, the deck may run out of cards. This case should be handled.

You can extend the activity and make it more interesting by altering the solution to include the following scenarios:

  • Bonus 1: Allow playing with more than one deck of cards or more than one player.
  • Bonus 2: After every hand, place the used cards in a discard array that can be used and shuffled when the main deck has run out.

Here is the expected output:

Figure 4.33: Output for Blackjack

Figure 4.33: Output for Blackjack

Note

The solution for this activity can be found on page 464.

Summary

In this chapter, we covered the significance of methods, how to define them, and the different ways to send arguments to them. Indeed, methods are one of the foundational concepts of Ruby, so it's important to feel comfortable using them. The main purpose of a method is to wrap up a chunk of code to accomplish a small task. You do not want to create methods with lots of code. If you do end up with a method that has lots of code, you can refactor it into multiple, smaller methods.

Methods take arguments and can return values. As long as the method signature and return values stay the same, it makes it very easy to change the implementation later on, which is a core virtue of methods.

Methods, like atoms, are building blocks of software programming. Once we start having a lot of methods, we will want to bundle them up into a higher-order concept. In Ruby, there are two higher-order concepts in which to group methods: classes and modules. We will look at both of these in the next chapter, beginning with classes.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.219.130