Previous | ToC | Up | Next |
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
And before Dan can ask me to do so, let me run it:
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:
you really would like to write this as
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!
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:
Now what you would like to write is
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:
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:
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.
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
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
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!
Erica: A great improvement over the old array addition!
Dan: Still, I can't say I like your new notation.
I'm still not happy about the asymmetry. Writing
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:
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:
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:
Carol: I don't think so. Let me try a simpler case:
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!
Carol: Well, enough talk: let's test my second version of array addition:
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:
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.
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
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:
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:
Carol: Yes, but let us test that as well:
13. Overloading the + Operator
13.1. A DRY Version of Modified Euler
|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
r1 = []
r.each_index{|k| r1[k] = r[k] + v[k]*dt}
13.3. Array Addition
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.
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?
|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.
class Array
def plus(a)
sum = []
self.each_index{|k| sum[k] = self[k]+a[k]}
return sum
end
end
13.4. Who is Adding What
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
?
a = a1.plus(a2)
you can indeed write
a = a1.plus a2
13.5. The plus Method
|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.
13.6. The + Method
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?
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
|gravity> irb
2.+ 3
5
8.* 4
32
quit
Erica: Wow, surprise. They work just like ordinary Ruby methods.
|gravity> irb
2. + 3
5
8. * 4
32
quit
Ah, you see, the zero is just added, like in Fortran.
|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.
13.8. Testing the + Method
|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.
|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.
13.9. A Vector 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.
|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?
|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 |