Previous ToC Up Next

13. Overloading the + Operator

13.1. A DRY Version of Modified Euler

Dan: Now how did we get into all this array stuff?

Erica: I wanted to move on to the leapfrog algorithm, but Carol brought up the DRY principle, Don't Repeat Yourself, insisting on first cleaning up the modified Euler code . . .

Carol: . . . which we haven't done yet, but now we're all set to do so! It is just a matter of translating the old file euler_modified_1000_steps.rb, introducing our array notation, just as we did for the code in euler_array_each_def.rb.

Here it is, in euler_modified_array.rb

 include Math
 
 def print_pos_vel(r,v)
   r.each{|x| print(x, "  ")}
   v.each{|x| print(x, "  ")}
   print "\n"
 end
 
 r = [1, 0, 0]
 v = [0, 0.5, 0]
 dt = 0.01
 print_pos_vel(r,v)
 
 1000.times{
   r2 = 0
   r.each{|x| r2 += x*x}
   r3 = r2 * sqrt(r2)
   a = r.map{|x| -x/r3}
   r1 = []                                              
   r.each_index{|k| r1[k] = r[k] + v[k]*dt}             
   v1 = []
   v.each_index{|k| v1[k] = v[k] + a[k]*dt}
   r12 = 0
   r1.each{|x| r12 += x*x}
   r13 = r12 * sqrt(r12)
   a1 = r1.map{|x| -x/r13}
   r2 = []
   r1.each_index{|k| r2[k] = r1[k] + v1[k]*dt}
   v2 = []
   v1.each_index{|k| v2[k] = v1[k] + a1[k]*dt}
   r.each_index{|k| r[k] = 0.5 * ( r[k] + r2[k] )}
   v.each_index{|k| v[k] = 0.5 * ( v[k] + v2[k] )}
   print_pos_vel(r,v)
 }

And before Dan can ask me to do so, let me run it:

 |gravity> ruby euler_modified_array.rb | tail -1
 0.400020239524913  0.343214474344616  0.0  -1.48390077762002  -0.0155803976141248  0.0  
I will also compare it to the previous result:

 |gravity> ruby euler_modified_1000_steps.rb | tail -1
 0.400020239524913  0.343214474344616  0.0  -1.48390077762002  -0.0155803976141248  0.0

13.2. Not quite DRY yet

Dan: Bravo! Same answers. And yes, the code has become shorter. I like that.

Erica: Hmmm, just making a code shorter does not necessarily make it better. You can use semicolons to put two or three lines on one line, but that doesn't make the code any prettier. Most likely, it will make the code more opaque.

In fact, I'm sorry to say, I don't find the new code easier to read. While the old code was longer, it was very easy to see what happened. But in the new code, there are all those [k] clumps floating around . . . I thought the whole point of using arrays was that we could hide the elements of the array!

Carol: To some extent, we have hidden things. The methods map, each and each_index can be attached directly to the arrays themselves, without showing elements. And our use of each in the print statements shows an example where there is no ugly [k] showing up at all. But I agree with you, we should be able to do better.

Erica: Can we really do much better? An array does seem to be the natural way to represent a physical vector quantity in a computer program. I've never seen any other way . . . ah, no, I take that back. I once saw a C++ program, where the writer had introduced a special vector class.

Carol: Can you remember what the reason was for doing so?

Erica: I'm not exactly sure now, but it may have had to do with making it easier to add vectors, by overloading the + operator, and that sort of thing.

Carol: That sounds exactly like what we need. If we take a look at the first line in our code that contains one of these ugly [k] notations that you so disliked:

   r1 = []                                              
   r.each_index{|k| r1[k] = r[k] + v[k]*dt}             

you really would like to write this as

   r1 = r + v*dt                     

right?

Erica: Yes, that would be great! I would love to get rid of all those [k] blocks. In fact, I think that we should get rid of them if we want to follow the DRY principle. Look, we have been repeating this [k] thingy three times in one line, in our latest code!

13.3. Array Addition

Carol: Fair enough. Well, it's always a good idea to start simply. The simplest case I can think of is to add two vectors. If we continue to represent them as arrays, we can add arrays a1 and a2 to obtain their sum a as follows:

  a = []
  a1.each_index{|k| a[k] = a1[k] + a2[k]}
which includes the declaration, which is necessary if a has not yet been introduced as an array, earlier in the program.

Now what you would like to write is

  a = a1 + a2
without any further complications that include references to elements [k] or to methods such as each_index or to a declaration of a, since after all the addition of two vectors should obviously produce a new vector. Right?

Erica: It sounds too good to be true, to be able to leave out all that crap, and to tell the computer only the minimal information needed, as if you were writing a note for yourself on a scratch pad. Do you really think you can implement all that, and even do away with the need for declarations?

Carol: I think so. First, let's see what happens if we don't make any modification. I must admit, I'm not sure what Ruby will do if we ask it to add two arrays. Well, let's find out:

 |gravity> irb
 a1 = [1, 2, 3]
 [1, 2, 3]
 a2 = [5, 6, 7]
 [5, 6, 7]
 a = a1 + a2
 [1, 2, 3, 5, 6, 7]
 quit
Dan: I guess that is one way to add two arrays, to just put them end to end, and string all the elements together. And for many applications that might just be the right thing to do, for example, if you have an array of the names of countries in a nation, and you want to add a few more names. But in our case, this is not what we want. We'd better get:

  a = [1+5, 2+6, 3+7] = [6, 8, 10]
Carol: Of course, we can change the definition of "+" for array.

Dan: How can you change the definition of a built-in function?

Carol: In Ruby you can change anything, or at least almost anything! But let's take only one step at a time. The simplest and safest way to get the correct addition behavior, is to introduce a new array method. We can call it Array#plus, and use it to add two arrays in the way Dan just specified, according to the vector rules that we have in mind for the physical addition of two vectors.

Erica: But how can you add a new method to the Array class? Somebody else already has defined the Array class for us. I guess we'll have to dig into wherever Ruby is defined, and change the Array class definition?

Carol: No digging needed! The neat thing about Ruby, one of the neat things, is that it allows you to augment a class. Even if someone else had defined a class, we can always add a few lines to the class definition, for example, when we want to add a method. The different bits and pieces of the class definition can live in different places. Nothing to worry about!

It should be simple. From what I've seen so far, I guess that Ruby wants us to write something like this, in file array_try_addition1.rb:

 class Array
   def plus(a)
     sum = []
     self.each_index{|k| sum[k] = self[k]+a[k]}
     return sum
   end
 end

This should add the method plus, that is doing the addition, to the other existing methods in the Array class. In this way, we don't disturb anything in the Array class, so everything should work as it did before. The only difference is that we can now add arrays in the way we intend for vectors.

13.4. Who is Adding What

Erica: That's a lot shorter and simpler than I had expected. It seems to deliver all the three things you promised: it hides the all [k] occurrences, it hides the each_index and it creates a new vector, so that you don't have to declare anything. And all that in just a few lines!

Dan: Not so fast. I'm not there yet. In fact, I don't understand this at all. To begin with, shouldn't addition have two arguments? You're going to add two vectors, no?

Carol: Yes, but here we're describing addition from the point of view of an array. Given one array, all we have to do is to specify one second array, which I have called a, which can then be added to the given array. The given array itself simply goes by the name of self, a reserved word that points to the current instance of the class.

Dan: You mean that the Array class definition describes arrays in general, but if I deal with a particular array, a1, then from the point of view of a1 itself, a1 is called self?

Carol: Right.

Dan: But we want to get the result a = a1 + a2. So from the point of view of a there really are two other arrays involved.

Carol: Yes, but at first we only have a1 and a2. That's all we've got, and that's what we have to use to construct a. The way I'm trying to define addition is by starting with one of the two arrays a1, and to define a method that allows a1 to accept a second array a2. So the whole operation can then be written as

  a1.plus(a2)
where a2 is the argument for the method plus that is associated with the class Array of which a1 is an instance. Now this expression will result a new instance of the Array class, and we can assign that new instance to the new variable a by writing:

  a = a1.plus(a2)
Dan: You can't write

  a = a1 plus a2
?

Carol: Sorry, no, you can't; that wouldn't make any sense. What you can do in Ruby is leave out the parentheses around the argument of a method. So instead of writing

  a = a1.plus(a2)
you can indeed write

  a = a1.plus a2

13.5. The plus Method

Dan: I'll take your word for it. So if we write this, a1 uses its own plus method, and according to the definition you wrote in the Array class, a1 first creates a new array called sum, which is an empty array, specified by writing []. Next it assigns the the value sum[k] = a1[k]+a2[k] to each component [k] of the array sum. That makes sense!

And finally, in the next line you return the value sum, before you reach the end of the method. In that way a receives the value that a1.plus a2 returns, which is the sum of a1 and a2. Okay, I got it now!

Carol: Let's hope it works, after everything I've told you!

 |gravity> irb
 require "array_try_addition1.rb"
 true
 a1 = [1, 2, 3]
 [1, 2, 3]
 a2 = [5, 6, 7]
 [5, 6, 7]
 a = a1.plus a2
 [6, 8, 10]
 quit
Dan: Wonderful! That's just what we ordered.

Erica: A great improvement over the old array addition!

13.6. The + Method

Dan: Still, I can't say I like your new notation. I'm still not happy about the asymmetry. Writing

  a = a1.plus a2
gives the impression that a1 is charging forward, gobbling up a2 and then spitting out the result. You told me that we cannot write

  a = a1 plus a2
and I understand that such a statement would have no clear meaning in Ruby. But is there really no way to make the expression more symmetric, rather than making a1 the predator and a2 the prey?

Carol: Actually, there is a way to make it at least look more symmetric. It is just a form of syntactic sugar, as they call it: a way to let the syntax look more tasty, without really changing the underlying code.

The idea is what is called `overloading operators'. We can use the + symbol, instead of the word plus, and we can redefine the meaning of + for the case of arrays. This is what I meant when I said earlier that in Ruby you can change almost anything. I have read about that; let me see whether it works. I believe the idea is to write something like this, in file array_try_addition2.rb:

 class Array
   def +(a)
     sum = []
     self.each_index{|k| sum[k] = self[k]+a[k]}
     sum
   end
 end

13.7. A Small Matter: the Role of the Period

Dan: All you've done is to change plus into + in the second line. Can that really work?

Erica: There is one more change: you've also left out the word return in the third line of the definition.

Carol: Ah yes, most people writing Ruby seem to leave out return; it is really not necessary to add that. You just have to remember to let the last line of a definition echo the result you want to return. The result of invoking a method is to return whatever the last line of the definition evaluates to.

And yes, other than that, I've just replaced plus by +. In fact, in all cases where Ruby uses +, it is only syntactic sugar for invoking a method that is associated with the left-hand side of the + symbol. So even though it looks symmetric, it never really has been a symmetric operation.

Dan: But how can it work? I thought that you always needed to write a dot between an object and its method.

Carol: Generally, that is true, and in fact, if you want to, you still can write the + operator using a period like normal Ruby methods.

Dan: Let me try:

 |gravity> irb
 2.+ 3
 5
 8.* 4
 32
 quit
Erica: Wow, surprise. They work just like ordinary Ruby methods.

Dan: Are you sure? Isn't 2. just translated into 2.0 so that we are only evaluating 2.0 + 3? Let's check, by adding a space after the periods:

 |gravity> irb
 2. + 3
 5
 8. * 4
 32
 quit
Ah, you see, the zero is just added, like in Fortran.

Carol: I don't think so. Let me try a simpler case:

 |gravity> irb
 2.
 quit
 NoMethodError: undefined method `quit' for 2:Fixnum
        from (irb):1
        from :0
 quit
You see: 2. is not translated into 2.0, but is in fact illegal. Or more accurately, it is okay in Ruby to leave space after the period between an object and its method. Here irb is asking for the method name, and doesn't like the fact that I just wanted to quit; irb interpreted the quit as the method name it was waiting for.

Dan: I agree, we can now be sure that 2.+ really invokes the addition operator of the number 2. Okay, now we know.

Carol: But of course, it is much more intuitively obvious to write 2 + 3 than to write 2.+ 3. I have mentioned earlier the principle of least surprise, introduced by Matsumoto, the designer of Ruby, as a guide line for the Ruby syntax. Even though in fact Ruby has much in common with Lisp, Matsumoto decided not to use a lisp like notation, in which 2 + 3 would have looked something like (+ (2, 3)), a beautifully clear notation once you get used to it, but unlike 2 + 3 not immediately obvious when somebody comes across it for the first time.

Dan: I'd say!

13.8. Testing the + Method

Carol: Well, enough talk: let's test my second version of array addition:

 |gravity> irb
 require "array_try_addition2.rb"
 true
 a1 = [1, 2, 3]
 [1, 2, 3]
 a2 = [5, 6, 7]
 [5, 6, 7]
 a = a1 + a2
 [6, 8, 10]
 quit
Dan: I like this a whole lot better! And I'm glad Matsumoto did not introduce four parentheses to add two things.

Carol: I like it too, but I'm afraid we can't leave things like this.

Dan: Why not?

Carol: Because we've now been tinkering with the Array class, we can no longer use arrays in the standard way. That's why not.

Erica: Ah, you mean that we can no longer concatenate arrays, the way we saw before, using the + method. What did we do again? It was something like this:

 |gravity> irb
 a1 = [1, 2, 3]
 [1, 2, 3]
 a2 = [4, 5, 6]
 [4, 5, 6]
 a = a1 + a2
 [1, 2, 3, 4, 5, 6]
 quit
Dan: Of course, that no longer will work, when we add our modification:

 |gravity> irb
 require "array_try_addition2.rb"
 true
 a1 = [1, 2, 3]
 [1, 2, 3]
 a2 = [4, 5, 6]
 [4, 5, 6]
 a = a1 + a2
 [5, 7, 9]
 quit
But who cares? I don't expect us to have much use for array concatenation anyway.

Carol: Ah, not so quick. I think you should care a lot! If you use any Ruby program, written by someone else, you don't know what that program relies on. Most likely some Ruby programs do rely on the default way of array addition, in the form of concatenation. If you're going to change the default rules, you're likely to invite disaster.

When we introduced the new plus method, there was no danger, since we left the existing methods, such as +, alone. That's fine. But tinkering with existing methods is simply a bad idea.

13.9. A Vector Class

Dan: Is there no way out? I like what we've done, and it would be a pity to give it up, now that we've just figured out how to do it.

Carol: Yes, there is a way. What we want to do is to introduce a new class, similar to the Array class, but with a different name and with somewhat different rules. To be precise, we want to define a vector class. Ruby has the convention that class names start with a capital, so a natural name for our new class would be Vector.

In principle, we could define our new class from scratch, but it would be a lot easier to use the features that the Array class already has. All we really want to do is to tame the Array class to behave like proper physical vectors, and we can do this by redefining only some of the array operations, such as adding two arrays, as we have just done, and multiplying an array with a scalar, and probably a few more such operations.

In Ruby, just like in C++ and many other object-oriented languages, we can do this by an approach called inheritance. Instead of just defining a new class

  class Vector
we can write

  class Vector < Array
which means that the new Vector class inherits all the features of the existing class Array. The Vector class is then called a subclass of the Array class, and the Array class is called a superclass of the Vector class.

Okay, let me see whether I can redefine the array addition operator, so that vectors can be added in the right way. From what I've seen so far, I guess that Ruby wants us to write something like this, in file vector_try_addition2.rb, to replace array_try_addition2.rb:

 class Vector < Array
   def +(a)
     sum = Vector.new
     self.each_index{|k| sum[k] = self[k]+a[k]}
     sum
   end
 end

This new class definition so far contains only one new method, by the name of +, that is doing the addition. Note that I create an empty new vector by writing Vector.new instead of [], the notation we used to create a new array. In fact, [] is simply syntactic sugar for Array.new. So therefore it is a straightforward change to replace it by Vector.new as I've done above.

We can use this new class in the same way as we did before, but there is one difference: we have to declare all objects we will play with to be vectors. Before, we declared objects as arrays by using the [] notation, which is really a shorthand for Array[]. Now we have to specify that they are vectors by writing Vector[]. At least I think that's how it works. Let's try:

 |gravity> irb
 require "vector_try_addition2.rb"
 true
 v1 = Vector[1, 2, 3]
 [1, 2, 3]
 v2 = Vector[5, 6, 7]
 [5, 6, 7]
 v = v1 + v2
 [6, 8, 10]
 quit
Dan: That seems to do the right thing. And I guess we have now left the Array class alone, without changing it in any way, right?

Carol: Yes, but let us test that as well:

 |gravity> irb
 require "vector_try_addition2.rb"
 true
 a1 = [1, 2, 3]
 [1, 2, 3]
 a2 = [5, 6, 7]
 [5, 6, 7]
 a = a1 + a2
 [1, 2, 3, 5, 6, 7]
 quit
Dan: Good. So we're now playing it safe.
Previous ToC Up Next