Objects, methods, and local variables - Ruby foundations - The Well-Grounded Rubyist, Second Edition (2014)

The Well-Grounded Rubyist, Second Edition (2014)

Part 1. Ruby foundations

Chapter 2. Objects, methods, and local variables

This chapter covers

· Objects and object orientation

· Innate versus learned object capabilities

· Method parameter, argument, and calling syntax

· Local variable assignment and usage

· Object references

In this chapter, we’ll begin exploring the details of the Ruby programming language. We’ll look first and foremost at the concept of the object, around which almost every line of Ruby code you’ll write will revolve. What you do with objects, broadly speaking, is send them messages, most of which correspond to names of methods that you’re asking the object to execute. We’ll look in considerable detail at the combined processes of message sending and method calling.

Ruby objects are often (perhaps most often) handled via variables that represent them, and in this chapter, we’ll get deeper technically than we have so far into the nature and behavior of variables in Ruby. Once you’ve worked through this chapter, you’ll have a firm foothold in the landscape of Ruby objects and their manipulation.

As always, you can type the code samples into irb, and/or store them in a file that you then run using the Ruby interpreter. Whichever approach you lean toward, it’s not a bad idea to keep an irb session open as you proceed—just in case.

2.1. Talking to objects

In any Ruby program, the bulk of the design, logic, and action revolves around objects. When you write Ruby programs, your main activities are creating objects, endowing them with abilities, and asking them to perform actions. Objects are your handle on the universe of your program. When you want something done—a calculation, an output operation, a data comparison—you ask an object to do it. Rather than ask in the abstract whether a equals b, you ask a whether it considers itself equal to b. If you want to know whether a given student is taking a class from a given teacher, you ask the student, “Are you a student of this teacher?” Exactly how this kind of querying plays out, in terms of data structures and syntax, depends on the specifics of your program design. But throughout, writing a Ruby program is largely a matter of engineering your objects so that each object plays a clear role and can perform actions related to that role.

2.1.1. Ruby and object orientation

Ruby comes to the idea of manipulating data through objects via the program-language design principle object orientation. Many extremely popular programming languages are object-oriented (such as Java, C++, and Python, as well as Ruby), and some languages that aren’t fully object-oriented have facilities for writing object-oriented code (for example, Perl, as described in Object-Oriented Perl by Damian Conway [Manning, 1999]). In object-oriented programming (OOP), you perform calculations, data manipulation, and input/output operations by creating objects and asking them to perform actions and provide you with information.

In most object-oriented languages, including Ruby, every object is an example or instance of a particular class, and the behavior of individual objects is determined at least to some extent by the method definitions present in the object’s class. We’ll explore classes in depth in chapter 3. Here, we’ll focus directly on objects.

The real world

The term real world gets thrown around a lot in discussions of programming. There’s room for debate (and there is debate) as to whether this or that programming language, or even this or that kind of programming language, corresponds more closely than others to the shape of the real world. A lot depends on how you perceive the world. Do you perceive it as peopled with things, each of which has tasks to do and waits for someone to request the task? If so, you may conclude that object-oriented languages model the world best. Do you see life as a series of to-do items on a checklist, to be gone through in order? If so, you may see a strictly procedural programming language as having closer ties to the characteristics of the real world.

In short, there’s no one answer to the question of what the real world is—so there’s no answer to the question of what it means for a programming language to model the real world. Nor is it necessarily as important an issue as it may seem. The world you construct in a computer program is at heart an imaginary world, and limiting yourself to making it seem otherwise can be overly constrictive.

Designing object-oriented software is largely a matter of figuring out what you want your objects to be: what they should do, how they’ll interact with each other, how many of each there should be (for example, many students, one registrar), and other such questions. As you’ll see, Ruby provides a complete set of tools for naming, creating, addressing, and manipulating objects—and, through the manipulation of those objects, the data they operate on.

2.1.2. Creating a generic object

At first, the concept of OOP tends to come across as both simple (you write programs that have books and bottles and cars and houses, and you orchestrate a kind of conversation among those things) and abstract (Object? What does that mean? What do I type into my program file to create a “house” object?). OOP does have a component of simplicity; it lets you draw on objects, entities, roles, and behaviors as a source for how you design your programs, and that can be a help. At the same time, to create and use objects in your programs, you have to learn how it’s done in a given language.

Note

Depending on your background and expectations, you may be wondering why we’re not starting our exploration of objects with a close look at classes rather than objects. Classes are important in Ruby; they’re a way to bundle and label behaviors (you can have a Person class, a Task class, and so on) and to create multiple objects with similar behaviors easily. But—and in this respect, Ruby differs from some other object-oriented languages—the real action is with the individual objects: every object has the potential to “learn” behaviors (methods) that its class didn’t teach it. The class concept fits on top of the object concept, not the other way around. In fact, a class in Ruby is itself an object! More later ... but that’s the basics of why we’re starting with objects.

Seeing a language-specific explanation of OOP can make the abstract parts easier to grasp. We’ll therefore proceed to some Ruby code. We’ll create a new object. It won’t represent or model anything specific, like a house or a book or a teacher; it will be a generic object:

obj = Object.new

Now you have an object and a variable through which you can address it.

All Ruby objects are created with certain innate abilities—certain methods that they know how to execute because they’re Ruby objects. Those abilities, although important, aren’t that exciting, so we’ll keep them to the side for the moment. More exciting is what happens when you teach your object how to do the things you want it to do.

Defining an object’s behavior

Let’s say you’ve created an object and you want it to do something interesting: you want it to talk. To get it to talk, you have to ask it to talk. But before you ask it to talk, you have to teach it how to talk.

Specifically, and more technically, you have to define a method for your object. You do this using a special term—a keyword—namely, the keyword def.

Here’s how to define the method talk for the object obj:

def obj.talk
puts "I am an object."
puts "(Do you object?)"
end

Figure 2.1 shows an analysis of that chunk of code.

Figure 2.1. Anatomy of a method definition

Now obj knows how to talk, and you can ask it to do so.

Sending messages to objects

To ask obj to talk, use the message-sending or method-calling syntax you encountered in chapter 1:

obj.talk

And it talks:

I am an object.
(Do you object?)

The object obj understands, or responds to, the message talk. An object is said to respond to a message if the object has a method defined whose name corresponds to the message.

A few things to consider about the dot-based message-sending syntax:

· The dot (.) is the message-sending operator. The message on the right is sent to the object (or receiver, as it’s often called in this role) on the left.

· The receiver can be, and often is, represented by a variable that stands in for an object. But a receiver can also be a literal object construct—for example, a string in quotation marks.

· In practice, the message being sent is almost always the name of a method (like talk, the method defined earlier). The object always tries to act on the assumption that the message is the name of a method. If there’s no method by that name, error-handling measures are taken.

The semantics of method calls let you go further than the relatively one-dimensional “talk” case, particularly when you start calling methods with arguments.

2.1.3. Methods that take arguments

Methods in Ruby are much like mathematical functions: input goes in, the wheels turn, and a result comes out. To feed input to a Ruby method, you call the method with one or more arguments.

In a method definition, you indicate the arguments by means of a list of variables in parentheses after the method name. (Arguments can be required or optional. We’ll look at required arguments here and optional arguments a little later.) When you call the method, you provide values corresponding to these variables in your method call. More precisely, the variables listed in the method definition are the method’s formal parameters, and the values you supply to the method when you call it are the corresponding arguments. (It’s common to use the word arguments, informally, to refer to a method’s parameters as well as a method call’s arguments, but it’s useful to know the technical distinction.)

Let’s say you want your object to function as a Celsius-to-Fahrenheit converter. You can teach it how to do the conversion by defining a method:

def obj.c2f(c)
c * 9.0 / 5 + 32
end

(This time 9 has become 9.0 in the conversion formula. That will force the result to be a float, which is more precise than an integer.) The method obj.c2f has one formal parameter, which means it takes one argument. When you call the method, you provide an argument:

puts obj.c2f(100)

The result is

212.0

As you can see, there’s a direct correspondence between the syntax of the parameter list in a method definition and the syntax of the argument list when the method is called. The parentheses are optional in both cases; you could do this

def obj.c2f c

and this:

obj.c2f 100

They’re not always optional, though, particularly when you’re stringing multiple method calls together, so it’s good to lean toward using them rather than leaving them out. You can make an exception for common or conventional cases where parentheses are usually excluded, like calls toputs. But when in doubt, use the parentheses.

At the other end of the process, every method call hands back—returns—a value.

2.1.4. The return value of a method

Ruby code is made up of expressions, each of which evaluates to a particular value. Table 2.1 shows some examples of expressions and their values (along with explanatory comments).

Table 2.1. Examples of Ruby expressions and the values to which they evaluate

Expression

Value

Comments

2 + 2

4

Arithmetic expressions evaluate to their results.

"Hello"

"Hello"

A simple, literal string (in quotation marks) evaluates to itself.

"Hello" + " there"

"Hello there"

Strings can be “added” to each other (concatenated) with the plus sign.

c = 100

100

When you assign to a variable, the whole assignment evaluates to the value you’ve assigned.

c * 9/5 + 32

212

The usual rules of precedence apply: multiplication and division bind more tightly than addition and are performed first.

obj.c2f(100)

212

A method call is an expression.

Look at the last entry in table 2.1: it’s a call to obj.c2f. Every method call is an expression. When you call a method, the method call evaluates to something. This result of calling a method is the method’s return value.

The return value of any method is the same as the value of the last expression evaluated during execution of the method. In the case of the temperature-conversion method, the last expression evaluated is the only line of the method body:

c * 9.0 / 5 + 32

Thus the result of that calculation provides the return value of the method.

Ruby gives you a keyword for making return values explicit: return. The use of this keyword is usually optional, but many programmers like to use it because it makes explicit what is otherwise implicit:

def obj.c2f(c)
return c * 9.0 / 5 + 32
end

This is equivalent to the earlier version of the method, but it’s more expressive about what it’s doing. On the other hand, it’s wordier. You have to decide, as a matter of your own style, whether you want to use return. You have to use it if you return multiple values, which will be automatically wrapped up in an array: return a,b,c rather than just a,b,c (though you can also return multiple values in an explicit array, like [a,b,c], without return). You also have to use return if you want to return from somewhere in the middle of a method. But whether you use returnor not, something will be returned from every method call. Even a call to an empty method body, consisting of just the def and end statements, returns nil.

At this point, the object is doing what we need it to do: listening to messages and acting on them. That’s a good illustration of how Ruby works, but it’s a scrappy one. We started with a generic object and taught it to talk and convert temperatures. That shows you the mechanics of defining and calling methods, but it results in a rather odd object. Let’s look at an object that models something a little more structured. We’ll handcraft a generic object so that it understands the behavior and business logic of a ticket to an event.

2.2. Crafting an object: The behavior of a ticket

A ticket is a familiar object, with a known set of properties and behaviors. Let’s take a high-level view at what we expect a ticket-like Ruby object to do and to know about itself.

2.2.1. The ticket object, behavior first

A ticket object should be able to provide data about itself. It should field requests for information about the event it’s for: when, where, name of event, performer, which seat, and how much it costs.

When asked, the ticket will provide the following information, based on an imaginary public reading by Mark Twain in 1903:

01/02/03
Town Hall
Author's reading
Mark Twain
Second Balcony, row J, seat 12
$5.50

The goal is to create an object from which we can easily get all this information.

Creating the ticket object

A generic object will serve as the basis for the ticket:

ticket = Object.new

Once it exists, we can start to endow the object ticket with properties and data by defining methods, each returning the appropriate value:

The majority of the methods defined here return string values. You can see this at a glance: they hand back a value inside quotation marks. The price method returns a floating-point number . Now that the ticket object knows a little about itself, let’s ask it to share the information.

2.2.2. Querying the ticket object

Rather than produce a raw list of items, let’s generate a reader-friendly summary of the details of the ticket. The use of print and puts can help get the information into more or less narrative form:

Save all the code, starting with ticket = Object.new, to a file called ticket.rb, and run it. You’ll see the following:

This ticket is for: Author's reading, at Town Hall, on 01/02/03.
The performer is Mark Twain.
The seat is Second Balcony, row J, seat 12, and it costs $5.50.

The code for this example consists of a series of calls to the methods defined earlier: ticket.event, ticket.venue, and so forth. The printing code embeds those calls—in other words, it embeds the return values of those methods ("Author's reading", "Town Hall", and so on)—in a succession of output commands, and adds connectors (", at", ", on", and so on) to make the text read well and look nice.

The Twain ticket is a simple example, but it encompasses some vital Ruby procedures and principles. The most important lesson is that the knowledge necessary for the program to do anything useful resides in the object. The ticket object has the knowledge; you tap into that knowledge by asking the ticket for it, via method calls. Nothing is more central to Ruby programming than this. It’s all about asking objects to do things and tell you things.

The ticket code works, and it embodies useful lessons; but it’s wordy. Ruby has a reputation as a powerful, high-level language. You’re supposed to be able to get a lot done with relatively little code. But the ticket example takes ten lines of print and puts instructions to generate three lines of output.

Let’s improve that ratio a bit.

2.2.3. Shortening the ticket code via string interpolation

One of the most useful programming techniques available in Ruby is string interpolation. The string-interpolation operator gives you a way to drop anything into a string: a variable, for example, or the return value of a method. This can save you a lot of back-and-forth between print andputs.

Moreover, strings can be concatenated with the plus sign (+). Here’s how the printing code looks, using string interpolation to insert the values of expressions into the string and using string addition to consolidate multiple puts calls into one:

puts "This ticket is for: #{ticket.event}, at #{ticket.venue}." +
"The performer is #{ticket.performer}." +
"The seat is #{ticket.seat}, " +
"and it costs $#{"%.2f." % ticket.price}"

Whatever’s inside the interpolation operator #{...} gets calculated separately, and the results of the calculation are inserted into the string. When you run these lines, you won’t see the #{...} operator on your screen; instead, you’ll see the results of calculating or evaluating what was between the curly braces. Interpolation helped eliminate 6 of 10 lines of code and also made the code look a lot more like the eventual format of the output, rather than something that works but doesn’t convey much visual information.

So far, we’ve been asking the ticket for information in the form of strings and numbers. Tickets also have some true/false—Boolean—information about themselves.

2.2.4. Ticket availability: Expressing Boolean state in a method

By way of Boolean information, consider the matter of whether a ticket has been sold or is still available. One way to endow a ticket with knowledge of its own availability status is this:

def ticket.availability_status
"sold"
end

Another way is to ask the ticket whether it’s available and have it report back true or false:

def ticket.available?
false
end

false is a special term in Ruby, as is the term true. true and false are objects. Ruby uses them to represent results of, among other things, comparison operations (like x > y), and you can use them to represent truth and falsehood in your own methods. You may have noticed that the method name available? ends with a question mark. Ruby lets you do this so you can write methods that evaluate to true or false and make the method calls look like questions:

if ticket.available?
puts "You're in luck!"
else
puts "Sorry--that seat has been sold."
end

But there’s more to truth and falsehood than the true and false objects. Every expression in Ruby evaluates to an object, and every object in Ruby has a truth value. The truth value of almost every object in Ruby is true. The only objects whose truth value (or Boolean value) is false are the object false and the special nonentity object nil. You’ll see Boolean values and nil in more detail in chapter 7. For the moment, you can think of both false and nil as functionally equivalent indicators of a negative test outcome.

Playing around with if expressions in irb is a good way to get a feel for how conditional logic plays out in Ruby. Try some examples like these:

(The first of these examples, if "abc", will generate a warning about string literals in conditions. You can ignore the warning for our present purposes.)

Notice how irb not only obeys the puts method calls but also, on its own initiative, outputs the value of the entire expression . In the cases where the puts happens, the whole expression evaluates to nil—because the return value of puts is always nil. In the last case, where the string isn’t printed (because the condition fails), the value of the expression is also nil—because an if statement that fails (and has no else branch to salvage it) also evaluates to nil .

Remembering that nil has a Boolean value of false, you can, if you wish, get into acrobatics with irb. A call to puts returns nil and is therefore false, even though the string gets printed. If you put puts in an if clause, the clause will be false. But it will still be evaluated. So,

>> if puts "You'll see this"
>> puts "but not this"
>> end
You'll see this
=> nil

The first puts is executed, but the value it returns, namely nil, isn’t true in the Boolean sense—so the second puts isn’t executed.

This is a contrived example, but it’s a good idea to get used to the fact that everything in Ruby has a Boolean value, and sometimes it’s not what you might expect. As is often the case, irb can be a great help in getting a handle on this concept.

Now that the ticket object has some handcrafted behaviors, let’s circle back and consider the matter of what behaviors every object in Ruby is endowed with at its creation.

2.3. The innate behaviors of an object

Even a newly created object isn’t a blank slate. As soon as an object comes into existence, it responds to a number of messages. Every object is “born” with certain innate abilities.

To see a list of innate methods, you can call the methods method (and throw in a sort operation, to make it easier to browse visually):

p Object.new.methods.sort

The result is a list of all the messages (methods) this newly minted object comes bundled with. (Warning: The output looks cluttered. This is how Ruby displays arrays—and the methods method gives you an array of method names. If you want a list of the methods one per line, use putsinstead of p in the command.)

[:!, :!=, :!~, :<=>, :==, :===, :=~, :__id__, :__send__, :class, :clone,
:define_singleton_method, :display, :dup, :enum_for, :eql?, :equal?, :extend,
:freeze, :frozen?, :hash, :inspect, :instance_eval, :instance_exec,
:instance_of?, :instance_variable_defined?, :instance_variable_get,
:instance_variable_set, :instance_variables, :is_a?, :kind_of?, :method,
:methods, :nil?, :object_id, :private_methods, :protected_methods,
:public_method, :public_methods, :public_send, :remove_instance_variable,
:respond_to?, :send, :singleton_class, :singleton_methods, :taint, :tainted?,
:tap, :to_enum, :to_s, :trust, :untaint, :untrust, :untrusted?]

Don’t worry if most of these methods make no sense to you right now. You can try them in irb, if you’re curious to see what they do (and if you’re not afraid of getting some error messages).

But a few of these innate methods are common enough—and helpful enough, even in the early phases of acquaintance with Ruby—that we’ll look at them in detail here. The following methods fit this description:

· object_id

· respond_to?

· send (synonym: __send__)

Adding these to your Ruby toolbox won’t be amiss, because of what they do and because they serve as examples of innate methods.

Generic objects vs. basic objects

Asking Ruby to create a new object for you with the Object.new command produces what we’re calling here, informally, a generic object. Ruby also has basic objects—and that’s a more formal name. If you call BasicObject.new, you get a kind of proto-object that can do very little. You can’t even ask a basic object to show you its methods, because it has no methods method! In fact, it has only seven methods—enough for the object to exist and be identifiable, and not much more. You’ll learn more about these basic objects in chapters 3 and 13.

2.3.1. Identifying objects uniquely with the object_id method

Every object in Ruby has a unique ID number associated with it. You can see an object’s ID by asking the object to show you its object_id, using this or similar code:

obj = Object.new
puts "The id of obj is #{obj.object_id}."
str = "Strings are objects too, and this is a string!"
puts "The id of the string object str is #{str.object_id}."
puts "And the id of the integer 100 is #{100.object_id}."

Having a unique ID number for every object can come in handy when you’re trying to determine whether two objects are the same as each other. How can two objects be the same? Well, the integer object 100 is the same as ... the integer object 100. (Ask 100 for its object ID twice, and the result will be the same.) And here’s another case:

a = Object.new
b = a
puts "a's id is #{a.object_id} and b's id is #{b.object_id}."

Even though the variables a and b are different, the object they both refer to is the same. (See section 2.5.1 for more on the concept of object references.) The opposite scenario can happen too: sometimes two objects appear to be the same, but they’re not. This happens a lot with strings. Consider the following example:

Even though these two strings contain the same text, they aren’t, technically, the same object. If you printed them out, you’d see the same result both times ("Hello"). But the string objects themselves are different. It’s like having two copies of the same book: they contain the same text, but they aren’t the same thing as each other. You could destroy one, and the other would be unaffected.

ID numbers and equality of objects

As in the case of human institutions, one of the points of giving objects ID numbers in Ruby is to be able to make unique identifications—and, in particular, to be able to determine when two objects are the same object.

Ruby provides a variety of ways to compare objects for different types of equality. If you have two strings, you can test to see whether they contain the same characters. You can also test to see whether they’re the same object (which, as you’ve just seen, isn’t necessarily the case, even if they contain the same characters). The same holds true, with slight variations, for other objects and other types of objects.

Comparing ID numbers for equality is just one way of measuring object equality. We’ll get into more detail about these comparisons a little later. Right now, we’ll turn to the next innate method on our list: respond_to?.

2.3.2. Querying an object’s abilities with the respond_to? method

Ruby objects respond to messages. At different times during a program run, depending on the object and what sorts of methods have been defined for it, an object may or may not respond to a given message. For example, the following code results in an error:

obj = Object.new
obj.talk

Ruby is only too glad to notify you of the problem:

NoMethodError: undefined method `talk' for #<Object:0x00000102836550>

You can determine in advance (before you ask the object to do something) whether the object knows how to handle the message you want to send it, by using the respond_to? method. This method exists for all objects; you can ask any object whether it responds to any message. respond_to?usually appears in connection with conditional (if) logic:

obj = Object.new
if obj.respond_to?("talk")
obj.talk
else
puts "Sorry, the object doesn't understand the 'talk' message."
end

respond_to? is an example of introspection or reflection, two terms that refer to examining the state of a program while it’s running. Ruby offers a number of facilities for introspection. Examining an object’s methods with the methods method, as we did earlier, is another introspective or reflective technique. (You’ll see many more such techniques in part 3 of the book.)

Up to now, we’ve used the dot operator (.) to send messages to objects. Nothing wrong with that. But what if you don’t know which message you want to send?

2.3.3. Sending messages to objects with the send method

Suppose you want to let a user get information from the ticket object by entering an appropriate query term (venue, performer, and so on) at the keyboard. Here’s what you’d add to the existing program:

print "Information desired: "
request = gets.chomp

The second line of code gets a line of keyboard input, “chomps” off the trailing newline character, and saves the resulting string in the variable request.

At this point, you could test the input for one value after another by using the double equal sign comparison operator (==), which compares strings based on their content, and calling the method whose value provides a match:

if request == "venue"
puts ticket.venue
elsif request == "performer"
puts ticket.performer
...

To be thorough, though, you’d have to continue through the whole list of ticket properties. That’s going to get lengthy.

There’s an alternative: you can send the word directly to the ticket object. Instead of the previous code, you’d do the following:

This version uses the send method as an all-purpose way of getting a message to the ticket object. It relieves you of having to march through the whole list of possible requests. Instead, having checked that the ticket object knows what to do , you hand the ticket the message and let it do its thing.

Using __send__ or public_send instead of send

Sending is a broad concept: email is sent, data gets sent to I/O sockets, and so forth. It’s not uncommon for programs to define a method called send that conflicts with Ruby’s built-in send method. Therefore, Ruby gives you an alternative way to call send: __send__. By convention, no one ever writes a method with that name, so the built-in Ruby version is always available and never comes into conflict with newly written methods. It looks strange, but it’s safer than the plain send version from the point of view of method-name clashes.

In addition, there’s a safe—but in a different way—version of send (or __send__) called public_send. The difference between plain send and public_send is that send can call an object’s private methods, and public_send can’t. We cover private methods later in the book, but in case you’re curious what public_send was doing in the method list, that’s the gist.

Most of the time, you’ll use the dot operator to send messages to objects. But the send alternative can be useful and powerful—powerful enough, and error-prone enough, that it almost always merits at least the level of safety-netting represented by a call to respond_to?. In some cases,respond_to? might even be too broad to be safe; you might only send a message to an object if the message is included in a predetermined message “whitelist.” The guiding principle is care: be careful about sending arbitrary messages to objects, especially if those messages are based on user choice or input.

Next, we’ll put method argument syntax and semantics under the microscope.

2.4. A close look at method arguments

Methods you write in Ruby can take zero or more arguments. They can also allow a variable number of arguments. We’ll examine argument semantics in several different ways in this section:

· The difference between required and optional arguments

· How to assign default values to arguments

· The rules governing the order in which you have to arrange the parameters in the method signature so that Ruby can make sense of argument lists in method calls and bind the parameters correctly

· What you can’t do with arguments in Ruby

Table 2.2 will summarize these at the end of this section.

Note

There’s more to argument list semantics than we’ll cover here. Specifically, there’s such a thing in Ruby as keyword arguments (or named parameters). That feature is strongly connected to the use of hashes as method arguments—which is why you won’t see a full explanation of it until we’ve talked about hashes in depth in chapter 9.

2.4.1. Required and optional arguments

When you call a Ruby method, you have to supply the correct number of arguments. If you don’t, Ruby tells you there’s a problem. For example, calling a one-argument method with three arguments

obj = Object.new

def obj.one_arg(x)
puts "I require one and only one argument!"
end

obj.one_arg(1,2,3)

results in

ArgumentError: wrong number of arguments (3 for 1)

It’s possible to write a method that allows any number of arguments. To do this, put a star (an asterisk, *) in front of a single argument name:

def obj.multi_args(*x)
puts "I can take zero or more arguments!"
end

The *x notation means that when you call the method, you can supply any number of arguments (or none). In this case, the variable x is assigned an array of values corresponding to whatever arguments were sent. You can then examine the values one at a time by traversing the array. (We’ll look more closely at arrays in chapter 9.)

You can fine-tune the number of arguments by mixing required and optional arguments:

def two_or_more(a,b,*c)
puts "I require two or more arguments!"
puts "And sure enough, I got: "
p a, b, c
end

In this example, a and b are required arguments. The final *c will sponge up any other arguments that you may send and put them into an array in the variable c. If you call two_or_more(1,2,3,4,5), you’ll get the following report of what got assigned to a, b, and c:

I require two or more arguments!
And sure enough, I got:
1
2
[3, 4, 5]

(Using p rather than print or puts results in the array being printed out in array notation. Otherwise, each array element would appear on a separate line, making it harder to see that an array is involved at all.)

You can also make an argument optional by giving it a default value.

2.4.2. Default values for arguments

When you supply a default value for an argument, the result is that if that argument isn’t supplied, the variable corresponding to the argument receives the default value.

Default arguments are indicated with an equal sign and a value. Here’s an example:

def default_args(a,b,c=1)
puts "Values of variables: ",a,b,c
end

If you make a call like this

default_args(3,2)

you’ll see this result:

Values of variables:
3
2
1

No value was supplied in the method call for c, so c was set to the default value provided for it in the parameter list: 1. If you do supply a third argument, that value overrides the default assignment of 1. The following call

default_args(4,5,6)

produces this result:

Values of variables:
4
5
6

The real fun starts when you mix and match the different elements of argument syntax and have to figure out what order to put everything in.

2.4.3. Order of parameters and arguments

What output would you expect from the following code snippet?

def mixed_args(a,b,*c,d)
puts "Arguments:"
p a,b,c,d
end

mixed_args(1,2,3,4,5)

You’ve seen that a starred parameter, like *c, sponges up the remaining arguments—at least, it did so in the method two_or_more, where *c occurred last in the parameter list. What happens when another argument follows it?

Basically, Ruby tries to assign values to as many variables as possible. And the sponge parameters get the lowest priority: if the method runs out of arguments after it’s performed the assignments of required arguments, then a catch-all parameter like *c ends up as an empty array. The required arguments both before *c and after *c get taken care of before *c does.

The output of the previous snippet is this:

Arguments:
1
2
[3, 4]
5

The parameters a and b get the first two arguments, 1 and 2. Because the parameter at the end of the list, d, represents a required argument, it grabs the first available value from the right-hand end of the argument list—namely, 5. Whatever’s left in the middle (3, 4) gets sponged up by c.

If you only give enough arguments to match the required arguments of the method, then the sponge array will be empty. The method call

mixed_args(1,2,3)

results in this output:

1
2
[]
3

In this example, c is out of luck; there’s nothing left.

You can get reasonably fancy with parameter syntax. Here’s a method that takes a required argument; an optional argument that defaults to 1; two more required arguments taken from the right; and, somewhere in the middle, everything else:

def args_unleashed(a,b=1,*c,d,e)
puts "Arguments:"
p a,b,c,d,e
end

And here’s an irb session that puts this method through its paces. Note that the return value of the method call, in every case, is an array consisting of all the values. That’s the return value of the call to p. It’s an array representation of the same values that you see printed out as individual values on separate lines:

The first call to args_unleashed has five arguments . That means there are enough to go around: b gets its default overridden, and the array c gets one element. The second call is stingier , and c loses out: b gets to override its default, leaving c empty (because the last two arguments are spoken for by the required arguments d and e).

The third call tightens its belt even further . This time, there are only enough arguments to satisfy the basic requirements—that is, something to assign to a, d, and e. The parameter b falls back on its default, and c is empty.

The fourth call goes the other way : this time, there are more arguments than the method requires and more than enough to populate the optional and default-valued parameters. It’s a bonanza for c, which does its job of sponging up all the arguments that aren’t needed elsewhere and ends up containing four elements.

The fifth call, on the other hand, doesn’t send enough arguments to satisfy the basic requirements . The variable bindings can’t be made, so you get an argument error.

Along with the nuts and bolts of argument syntax, the most important thing to take away from these examples is the perhaps obvious point that, no matter what you do, every parameter ends up bound to some value. There’s no such thing as a parameter that just sort of floats away into nothingness. If it’s in the list, it ends up as a local variable inside the method—even if it’s just bound to an empty array, like c sometimes is. You may or may not use every such variable, but the bindings are always made.

Figure 2.2 offers a graphic representation of the basic logic of argument assignment. The listing of assignments in the box shows the order of priority: required arguments are handled first, then the default-valued optional argument, and then the sponge.

Figure 2.2. Argument assignment logic in action

If you have complex argument needs, you must follow the rules carefully—and also keep in mind what you can’t do.

2.4.4. What you can’t do in argument lists

Parameters have a pecking order. Required ones get priority, whether they occur at the left or at the right of the list. All the optional ones have to occur in the middle. The middle may be the middle of nothing:

And you can have required arguments on the left only or on the right only—or both.

What you can’t do is put the argument sponge to the left of any default-valued arguments. If you do this,

def broken_args(x,*y,z=1)
end

it’s a syntax error, because there’s no way it could be correct. Once you’ve given x its argument and sponged up all the remaining arguments in the array y, nothing can ever be left for z. And if z gets the right-hand argument, leaving the rest for y, it makes no sense to describe z as “optional” or “default-valued.” The situation gets even thornier if you try to do something like the equally illegal (x, *y, z=1, a, b). Fortunately, Ruby doesn’t allow for more than one sponge argument in a parameter list. Make sure you order your arguments sensibly and, when possible, keep your argument lists reasonably simple!

Table 2.2 summarizes what you’ve learned so far about argument syntax and semantics. You can treat this table more as a reference than as something you need to commit to memory and quiz yourself on—as long as you follow the basic reasoning of why each example works as it does.

Table 2.2. Sample method signatures with required, optional, and default-valued arguments

Argument type(s)

Method signature

Sample call(s)

Variable assignments

Required (R)

def m(a,b,c)

m(1,2,3)

a = 1, b = 2, c = 3

Optional (O)

def m(*a)

m(1,2,3)

a = [1,2,3]

Default-valued (D)

def m(a=1)

m
m(2)

a = 1
a = 2

R/O

def m(a,*b)

m(1)

a = 1, b = [ ]

R/D

def m(a,b=1)

m(2)
m(2,3)

a = 2, b = 1
a = 2, b = 3

D/O

def m(a=1,*b)

m
m(2)

a = 1, b = []
a = 2, b = [ ]

R/D/O

def m(a,b=2,*c)

m(1)
m(1,3)
m(1,3,5,7)

a = 1, b = 2, c = []
a = 1, b = 3, c = [ ]
a = 1, b = 3, c = [5,7]

R/D/O/R

def m(a,b=2,*c,d

m(1,3)
m(1,3,5)
m(1,3,5,7)
m(1,3,5,7,9)

a = 1, b = 2, c = [ ], d = 3
a = 1, b = 3, c = [ ], d = 5
a = 1, b = 3, c = [5], d = 7
a = 1, b = 3, c = [5,7], d = 9

As you can see from table 2.2, the arguments you send to methods are assigned to variables—specifically, local variables, visible and usable for the duration of the method. Assignment of local variables through method argument binding is just one case of the general process of local variable assignment, a process that we’ll look at in detail next.

2.5. Local variables and variable assignment

Local variable names start with a lowercase letter or an underscore and are made up of alphanumeric characters and underscores. All of these are valid local variable names, including the lone underscore:

x
_x
name
first_name
plan9
user_ID
_

The local in local variables pertains to the fact that they have limited scope: a local variable is only visible in a limited part of a program, such as a method definition. Local variable names can be reused in different scopes. You can use, say, the variable name x in more than one place, and as long as those places have different scopes, the two x variables are treated as completely separate. (Remember that conventional Ruby style prefers under_score names over camelCase names for local variables.)

Scope is an important topic in its own right, and we’ll get deeply into it in chapter 5. You can start getting familiar with some key aspects of it now, though, as you examine how local variables come and go. The classic case of local scope is a method definition. Watch what happens with x in this example:

The output from this program is as follows:

Hello
Goodbye
Let's check whether x remained the same:
Hello

When you call start_here , the method start_here is executed. Inside that method, the string Hello is assigned to x —that is, to this x, the x in scope inside the method.

start_here prints out its x (Hello) and then calls the method say_goodbye . In say_goodbye, something similar happens: a string (Goodbye) is assigned to x . But this is a different x—as you see when the call to say_goodbye is finished and control returns to start_here: Ruby prints out thisx, and the value is still Hello . Using x as a local variable name in the scope of one method didn’t affect its value in the scope of the other.

The local variables in this last example are created through explicit assignment. (Local variables can come into being, as you’ve seen, through the binding of method arguments to method parameters.) But what happens when the assignment or initialization takes place? What exactly is the relation between a variable and the object that it represents?

2.5.1. Variables, objects, and references

Variable assignments give the appearance, and have the apparent effect, of causing the variable on the left to be set equal to the object on the right. After this assignment, for example,

str = "Hello"

statements like puts str will deliver the string "Hello" for printing and processing.

Now, look at this example:

str = "Hello"
abc = str
puts abc

This, too, prints "Hello". Apparently the variable abc also contains "Hello", thanks to having had str assigned to it.

But there’s more to it. The next example involves a method called replace, which does an in-place replacement of a string’s content with new text:

str = "Hello"
abc = str
str.replace("Goodbye")
puts str
puts abc
def say_goodbye
str = "Hello"
abc = str
str.replace("Goodbye")
puts str
puts abc
end

say_goodbye

Look closely at the output:

Goodbye
Goodbye

The first "Goodbye" is str; the second is abc. But we only replaced str. How did the string in abc get replaced?

Enter references

The answer is that variables in Ruby (with a few exceptions, most notably variables bound to integers) don’t hold object values. str doesn’t contain "Hello". Rather, str contains a reference to a string object. It’s the string object that has the characteristic of containing the letters that make up"Hello".

In an assignment with a variable name on the left and an object on the right, the variable receives a reference to the object. In an assignment from one variable to another (abc = str), the variable on the left receives a copy of the reference stored in the variable on the right, with the result that both variables now contain references to the same object.

The fact that variables hold references to objects has implications for operations that change objects. The string-replace operation

str.replace("Goodbye")

replaces the characters of the string to which str is a reference with the text "Goodbye". The variable abc contains another reference to the same string object. Even though the replace message goes to str, it causes a change to the object to which the reference in abc refers. When you print outabc, you see the result: the contents of the string have changed.

The un-reference: Immediate values

Some objects in Ruby are stored in variables as immediate values. These include integers, symbols (which look like :this), and the special objects true, false, and nil. When you assign one of these values to a variable (x = 1), the variable holds the value itself, rather than a reference to it.

In practical terms, this doesn’t matter (and it will often be left as implied, rather than spelled out repeatedly, in discussions of references and related topics in this book). Ruby handles the dereferencing of object references automatically; you don’t have to do any extra work to send a message to an object that contains, say, a reference to a string, as opposed to an object that contains an immediate integer value.

But the immediate-value representation rule has a couple of interesting ramifications, especially when it comes to integers. For one thing, any object that’s represented as an immediate value is always exactly the same object, no matter how many variables it’s assigned to. There’s only one object 100, only one object false, and so on.

The immediate, unique nature of integer-bound variables is behind Ruby’s lack of pre- and post-increment operators—which is to say, you can’t do this in Ruby:

x = 1
x++ # No such operator

The reason is that due to the immediate presence of 1 in x, x++ would be like 1++, which means you’d be changing the number 1 to the number 2—and that makes no sense.

For every object in Ruby, there can and must be one or more references to that object. If there are no references, the object is considered defunct, and its memory space is released and reused.

If you have two or more variables containing references to a single object, you can use any of them, on an equal basis, to send messages to the object. References have a many-to-one relationship to their objects. But if you assign a completely new object to a variable that’s already referring to an object, things change.

2.5.2. References in variable assignment and reassignment

Every time you assign to a variable—every time you put a variable name to the left of an equal sign and something else on the right—you start from scratch: the variable is wiped clean, and a new assignment is made.

Here’s a new version of our earlier example, illustrating this point:

str = "Hello"
abc = str
str = "Goodbye"
puts str
puts abc

This time the output is

Goodbye
Hello

The second assignment to str gives str a reference to a different string object. str and abc part company at that point. abc still refers to the old string (the one whose contents are "Hello"), but str now refers to a different string (a string whose contents are "Goodbye").

The first version of the program changed a single string, but the second version has two separate strings. After it’s reused, the variable str has nothing further to do with the object it referred to previously. But reusing str has no effect on abc, which still contains a reference to the original string.

Note

The examples use local variables to demonstrate what does and doesn’t happen when you assign to a variable that’s already been assigned to. But the rules and behaviors you’re seeing here aren’t just for local variables. Class, global, and instance variables follow the same rules. (So do so-called constants, which you can assign to more than once, oddly enough!) All of these categories of identifier are l-values: they can serve as the left-hand side, or target, of an assignment. (Compare with, say, 100 = 10, which fails because 100 isn’t an l-value.) And they all behave the same with respect to how they bind to their right-hand side and what happens when you use a given one more than once.

Ruby variables are often described as labels or names for objects. It’s a useful comparison. Say you have two names for your dog. “I’m taking Fido to the vet” and “I’m taking Rover to the vet” refer to the same animal. But if you get a new dog and transfer the name Fido to him, then the name-to-dog bindings have changed. Fido and Rover no longer refer to the same animal, and the name Fido has no further connection with the first dog.

And the new Fido doesn’t even have to be a dog; you could stop calling your dog Fido and start using the name for your car instead. It’s the same when you do x = 1 followed by x = "A string". You’re reusing the identifier x for a completely new class of object (String rather than Fixnum). Unlike some languages, Ruby doesn’t have typed variables. Any variable can be bound to any object of any class at any time.

The semantics of references and (re)assignment have important implications for how things play out when you call a method with arguments. What does the method receive? And what can the method do with it?

2.5.3. References and method arguments

Let’s stick with a string-based example, because strings are easy to change and track. Here’s a method that takes one argument:

def change_string(str)
str.replace("New string content!")
end

Next, create a string and send it to change_string:

s = "Original string content!"
change_string(s)

Now, examine s:

puts s

The examination reveals that the contents of the string to which s refers have changed:

New string content!

This tells you that inside the change_string method, the variable str is assigned a reference to the string also referred to by s. When you call a method with arguments, you’re really trafficking in object references. And once the method has hold of a reference, any changes it makes to the object through the reference are visible when you examine the object through any of its references.

Ruby gives you some techniques for protecting objects from being changed, should you wish or need to do so.

Duping and freezing objects

If you want to protect objects from being changed inside methods to which you send them, you can use the dup method, which duplicates an object:

You can also freeze an object, which prevents it from undergoing further change:

Note that there’s no corresponding unfreeze method. Freezing is forever.

To complete the picture, there’s also a method called clone. It’s a lot like dup. The difference is that if you clone a frozen object, the clone is also frozen—whereas if you dup a frozen object, the duplicate isn’t frozen.

With these tools in hand—dup, clone, and freeze—you can protect your objects against most rogue change operations. Some dangers still lurk, though. Even if you freeze an array, it’s still possible to change the objects inside the array (assuming they’re not frozen):

In this example, the fact that the numbers array is frozen means you can’t change the array . But the strings inside the array aren’t frozen. If you do a replace operation on the string "three", mischievously turning it into "four" , the new contents of the string are revealed when you reexamine the (still frozen!) array .

Be careful with references, and remember that a reference to an object inside a collection isn’t the same as a reference to the collection. (You’ll get a strong feel for collections as objects in their own right when we look at them in detail in chapter 9.)

A final point about variables—local variables in particular—involves their physical resemblance to method calls, and how Ruby figures out what you mean when you throw a plain, unadorned identifier at it.

2.5.4. Local variables and the things that look like them

When Ruby sees a plain word sitting there—a bareword identifier, like s, ticket, puts, or user_name—it interprets it as one of three things:

· A local variable

· A keyword

· A method call

Keywords are special reserved words that you can’t use as variable names. def is a keyword; the only thing you can use it for is to start a method definition. (Strictly speaking, you can trick Ruby into naming a method def. But...well...don’t.) if is also a keyword; lots of Ruby code involves conditional clauses that start with if, so it would be confusing to also allow the use of if as a variable name. A sequence like if = 3 would be difficult for Ruby to parse.

Like local variables, method calls can be plain words. You’ve seen several examples, including puts and print. If the method call includes arguments in parentheses—or even empty parentheses—then it’s clear that it’s not a local variable. In other cases, there may be some ambiguity, and Ruby has to figure it out.

Here’s how Ruby decides what it’s seeing when it encounters a plain identifier:

1. If the identifier is a keyword, it’s a keyword (Ruby has an internal list of these and recognizes them).

2. If there’s an equal sign (=) to the right of the identifier, it’s a local variable undergoing an assignment.

3. Otherwise, the identifier is assumed to be a method call.

If you use an identifier that isn’t any of these three things, then Ruby will complain and halt execution with a fatal error. The error message you get when this happens is instructive:

$ ruby -e "x"
-e:1:in `<main>': undefined local variable or method 'x' for main:Object
(NameError)

Note that Ruby can’t tell whether you thought x was a variable or a method. It knows that x isn’t a keyword, but it could be either of the other two. So the error message includes both.

At this point, you’ve got a large, growing store of knowledge about objects and variables and how they’re related. We’ll turn next in chapter 3 to the topic of how to create objects in a structured, scalable way with classes.

2.6. Summary

We’ve covered a lot of ground in chapter 2. In this chapter you saw

· How to create a new object and define methods for it

· The basics of the message-sending mechanism by which you send requests to objects for information or action

· Several of the important built-in methods that every Ruby object comes with: object_id, respond_to?, and send

· Details of the syntax for method argument lists, including the use of required, optional, and default-valued arguments

· How local variables and variable assignment work

· Ruby’s use of references to objects and how references play out when multiple variables refer to the same object

Writing a Ruby program can involve thinking about how you might map elements of a domain (even a modest one-entity domain like “a ticket to an event”) onto a system of objects so that those objects can store information and perform tasks. At the same time, it’s important not to think too rigidly about the relation between objects and the real world. Object-oriented languages certainly offer a strong component of real-world modeling; but Ruby, at the same time, is extremely elastic in its modeling facilities—as you can see from how easy it is to enhance a given object’s behavior. The chief goal in designing a program and the objects inside it is to come up with a system that works and that has internal consistency.

And, of course, the language offers lots of facilities for developing program structure. Creating objects one by one, as we’ve done in this chapter, is little more than the tip of the iceberg. We’ll expand the discussion exponentially next, by looking at how to create objects on a multiple, more automated basis using Ruby classes.