The Ruby Workshop
上QQ阅读APP看书,第一时间看更新

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

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

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

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

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

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

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

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.