- Evaluation Strategies and Method Arguments
- Argument Passing: Call-by-Value vs. Call-by-Reference
- Language vs. Implementation
- Call-by-Sharing
- References
Argument Passing: Call-by-Value vs. Call-by-Reference
In order to demonstrate the two major ways by which arguments are passed to methods, I'll use other programming languages in which the semantics are well defined and understood. Once we have a grasp of how argument passing works in another language, we'll compare it to Ruby and see how things fit. It's okay if you don't know the example languages; the code will be simple enough to understand what's happening and why it matters.
Of the two major argument-passing strategies, call-by-value is the easiest to understand. When a programming language uses call-by-value, each of the actual arguments is evaluated, and the resulting values are copied into the corresponding formal argument. The method's formal arguments are completely separate from its actual arguments; modifications to one don't affect the other. Consider the following C example (Figure 3), which always uses call-by-value:
#include <stdio.h> struct Position { int line; int column; }; void next_line (struct Position pos) { pos.line += 1; } int main (int argc, char *argv[]) { struct Position loc = {1, 1}; printf(“before new_line: %d\n”, loc.line); next_line(loc); printf(“after new_line: %d\n”, loc.line); return 0; }
The main function creates a new Position record, initializing the line member to 1. It then prints out the value of the line member (1) and calls the new_line function. With just a cursory glance at the new_line function, you might conclude that it assigns a new value to the line member in a way that would be visible to the main function. Of course, this isn't the case.
Thanks to call-by-value, the actual argument (loc) is copied into the formal argument (pos). When the line member is incremented in the new_line function, only the local copy is affected. The value in main remains unchanged. The two are completely separate and isolated values in memory. The two calls to printf from inside main produce output confirming that loc.line is always set to 1.
This style doesn't sound like Ruby at all, as demonstrated by the very first example code listing. If the next_line example was written in Ruby instead of C, the final value of loc.line would be 2. However, this doesn't mean that we can dismiss call-by-value entirely. Some Ruby programmers passionately take the position that Ruby does indeed use call-by-value. Let's table that idea for now and return to it shortly.
The next stop on our tour of argument-passing styles is call-by-reference. This style is a bit trickier. When a method is called, the value of the actual argument isn't directly provided as the value of the formal argument. Instead, the memory address of its value is passed around. Thus, the actual argument and the formal argument share the same value in memory. Changes to the formal argument can be seen through the actual argument.
While this description sounds exactly like how Ruby works, there's a catch. Most formal definitions of call-by-reference have another requirement: assigning a new value to a formal argument should also change the actual argument. [3] [4] Here's what that requirement would mean for Ruby (Figure 4):
def reassign (string) string = “reassign” end var = “initial” reassign(var)
Figure 4
In order for Ruby to qualify as call-by-reference, the final value of the var variable would have to be “reassign”. But that's not what happens. Running this code and peeking at the value of the var variable would show that it still has its initial value.
The reassignment requirement is a strange one, and as far as I can tell, it isn't well known or even universally accepted. On my first encounter with this requirement, I was skeptical that the definition of call-by-reference was correct. After reading several books and articles, I became convinced, but I still wanted to see an example from a mainstream language. That's when I remembered my old friend C++. Sure enough, that's exactly how references work. See for yourself (Figure 5):
#include <iostream> #include <string> void reassign (std::string &s) { s = std::string(“reassign“); } int main (void) { std::string var(“main”); std::cout << var << std::endl; reassign(var); std::cout << var << std::endl; return 0; }
Unlike in the Ruby version of this example, the final value of the var variable is “reassign”. When the reassign function use the assignment operator with its formal argument, it also changes the value of its actual argument. (C++ normally uses call-by-value. The ampersand (&) in front of the formal argument changes it to call-by-reference.)
Clearly, Ruby doesn't use call-by-reference. It only seems like it does until you consider the reassignment requirement. We're going to have to look elsewhere to understand how Ruby passes objects. But before we do that, let's take a detour and reconsider call-by-value.