Previous | ToC | Up | Next |
Carol: So where were we? We wanted to declare a as an array.
more precisely as an instance of the Array class. Here is one way to
do that:
Let me write the new version in a new file, euler_array.rb,
in the hope things will be correct now:
Dan: Seeing is believing. Does it now work?
Carol: Let's try:
Carol: Well, let's check:
In general, a Ruby class can have methods that are associated with
that class. A while ago, we have come across a very simple in
section 5.4, where we encountered the method times
that was associated with the class Fixnum. By writing 10.times
we could cause a loop to be transversed ten times.
Ruby has a somewhat confusing notation (class name)#(method name)
to describe methods that are associated with classes. The example
10.times is a way to invoke the method Fixnum#times.
I find it a bit confusing, because in practice you always use the dot
notation (object name).(method name) in your code. You'll never
see the # notation in a piece of code; you only encounter it
in a manual or other text description of a code.
Back to our application.
There are three methods for the class Array that we can use right away,
namely Array#each, Array#each_index and Array#map.
I'll explain what they all do in a moment, but it may be easiest to show
how they work in our forward Euler example, in file
euler_array_each.rb:
Erica: That looks nice and compact.
Dan: Does it work?
Carol: Let's see:
Erica: Good! Now let's look at these magic terms. I can guess what
each does. It seems to iterate over all the elements of an array,
applying whatever appears in parentheses to each element.
Carol: Yes, indeed. And while working with a specific element, it
needs to give that element a name. The name is defined between the two
vertical bars that follow the opening parentheses. It works just like
the lambda notion in Lisp.
Dan: I've no idea what lambda notation means, but I can see what is
happening here. In the line
writing {|x| ...} lets x stand for the element of the array
r. First x = r[0], and the ... command then becomes
print(r[0], " "). Then in the next round, x = r[1],
and so on.
Hey, now that I'm looking at the code a bit longer, I just noticed that
the construction .each{|x| ...} is actually quite similar to the
construction .times{...} that we use in the loop.
Carol: Yes, in both cases we are dealing with a method, each and times
that causes the statements in parentheses to be iterated. And the analogy
goes further. Remember that we learned how to get sparse output? At that
time we added a counter to the times loop, so that it became
.times{|i| ...}. Just like x stands in for each successive array
element in r.each{|x| ...}, so i stands in for each successive value
between 0 and 999 in 1000.times{|i| ...}.
Erica: As for your second magic term, the method each_index
seems to do something similar to each. What's the difference?
Carol: Take the line:
There we want to add to each element of array r the corresponding element
of array v, multiplied by dt. However, we cannot just use the
each method, since in that case we would iterate over the values
of r, and the dummy parameter, defined between vertical bars, will
take on the values r[0], r[1], and so on. That would
give us no handle on the value of the index, which is 0 in the
first case, 1 in the second, and so on.
In the print case above, we had no need to know the value of the index of
each element of the array that we were printing. But here, the value
of the index is needed, otherwise we cannot line up the corresponding
elements of r and v.
Erica: I see. Or at least I think I do. Let me try it out, using irb.
Dan: Why do we get an echo of the whole array, at the end of each result?
Carol: That's because irb always prints the value of an expression.
First the expression is evaluated, and as a side effect the print statements
in the expression are executed. But then a value is returned, which turns out
to be the array a itself. That's not particularly useful here, but in
general, it is convenient that irb always gives you the value of anything
it deals with, without you having to add print statements everywhere.
Dan: What about this mysterious map that you are using in line:
Carol: Ah, that is another Lisp like feature, but don't worry about that,
since you're not familiar with Lisp. The method map, when applied
by a given array, returns a new array in which every element is the
result of a mapping that is applied to the corresponding element of
the old array. That sounds more complicated than it really is.
Better to look at an example:
Carol: Yes, and notice how convenient it is that irb echoes the value
of each statement you type. You don't have to write
print a.map{|x| x + 1}, for example.
So in our case the line
transforms the old array r into a new array a for which each element
gets a minus sign and is divided by r3, which is just what we
needed.
Erica: Ah, look, you forgot to include the line a = [], and
it still worked, this time. That must be because now we are actually
producing a new array a, and we are no longer trying to assign values
to elements of a as we did before.
Carol: That's right! I had not even realized that. Good. One less
line to worry about.
Oh, by the way, when you look at books about Ruby, or when you happen
to see someone else's code, you may come across the method
Array#collect. That is just another name for Array#map.
Both collect and map are interchangeable terms. This often happens
in Ruby: many method names are just an alias for another method name.
I guess the author of Ruby tried to please many of his friends, even
though they had different preferences.
Erica: I prefer the name map, since it gives you the impression
that some type of transformation is being performed. As for the word
collect, it does not suggest much work being done.
Carol: I agree, and that's why I chose to use map here.
Erica: Carol, you convinced us that we should obey the DRY principle,
and indeed, we are no longer repeating ourselves on the level of vector
components. But when I look at the last code that you produced, there
is still a glaring violation of the DRY principle. Look at the three lines
that we use to print the positions and velocities right at the beginning.
The very same three lines are used inside the loop, at the end.
Carol: Right you are! Let's do something about that. Time to define
a method of our own. Here, this is easy. Let's introduce a method
called print_pos_vel(r,v), which prints the position and velocity
arrays. It has two arguments, r and v, the values of the two arrays
it should print.
We can write the definition of print_pos_vel at the top of the
file, and then we can invoke that method wherever we need it; I'll call
the file euler_array_each_def.rb:
Erica: Good! I think we can now certify this program as DRY compliant.
Dan: Does it work?
Carol: Ah yes, to be really compliant, it'd better work. Here we go:
Dan: I wonder, would it be possible to make the code even shorter?
Erica: Making a code shorter doesn't necessarily make it more readable!
Dan: Sure, but I'm just curious.
Carol: Well, if you want to get fancy, there is an array method called
inject. It's a strange name for what is something like an accumulation
method. Let me show you:
Carol: Indeed. So this will allow me to make the loop part of the code
a bit shorter, in euler_array_inject1.rb:
Dan: I see. That got rid of the first line of the previous loop code.
Does it work?
Carol: Good point, let's first test it:
Dan: And above that, you combined the assignment of the position and velocity
arrays. I'm surprised that that works!
Carol: In general, in Ruby you can
assign values to more than one variable in one statement, where Ruby
assumes that the values are listed in an array:
Carol: I takes a nested array, and replaces it by a flat array, where
all the components of the old tree structure are now arranged in one
linear array. Here's an example:
Carol: Ah, but I can do better! How about this one,
euler_array_inject2.rb?
Dan: Two lines less. You're getting devious! And does it work?
Carol: Beats me. Strange. Let's show a bit more output:
and here are the results:
Dan: Almost the same: now every line has a few blank spaces at the start.
Carol: Actually, that looks more elegant, doesn't it?
Erica: Frankly, I'm getting a bit tired of shaving lines from codes.
Stop playing, you two, and let's move one! 12. Array Methods
12.1. An Array Declaration
12.2. Three Array Methods
|gravity> ruby euler_array.rb | tail -1
7.6937453936572 -6.27772005661599 0.0 0.812206830641815 -0.574200201239989 0.0
Dan: Great! And it would be even better if this is what we got before.
|gravity> ruby euler.rb | tail -1
7.6937453936572 -6.27772005661599 0.0 0.812206830641815 -0.574200201239989 0.0
So far, so good. Okay, we got our first array-based version of
forward Euler working, but it still looks just like the old version.
Time to start using some of the power of Ruby's arrays.
|gravity> ruby euler_array_each.rb | tail -1
7.6937453936572 -6.27772005661599 0.0 0.812206830641815 -0.574200201239989 0.0
12.3. The Methods each and each_index
r.each_index{|k| r[k] += v[k]*dt}
|gravity> irb
a = [4, 7, 9]
[4, 7, 9]
a.each{|x| print x, "\n"}
4
7
9
[4, 7, 9]
a.each_index{|x| print x, "\n"}
0
1
2
[4, 7, 9]
a.each_index{|x| print a[x], "\n"}
4
7
9
[4, 7, 9]
quit
Yes, that makes sense.
12.4. The map Method
|gravity> irb
a = [4, 7, 9]
[4, 7, 9]
a.map{|x| x + 1}
[5, 8, 10]
a.map{|x| 2 * x}
[8, 14, 18]
quit
Dan: Ah, now I get it. In the first case, the mapping is simply adding
the number one, and indeed, each element of the array gets increased
by one. And in the second case, the mapping tells us that any element x
is doubled to become 2 * x, and that's exactly what happens.
12.5. Defining a Method
|gravity> ruby euler_array_each_def.rb | tail -1
7.6937453936572 -6.27772005661599 0.0 0.812206830641815 -0.574200201239989 0.0
12.6. The Array#inject Method
|gravity> irb
a = [3, 4, 5]
[3, 4, 5]
a.inject(0){|sum, x| sum + x}
12
a.inject(1){|product, x| product * x}
60
quit
Erica: I get the idea. What inject(p){|y, x| y @ x}
does is to give y the initial value p, and then for each array
component x, it applies the @ operator, whatever it is,
to the arguments y and x.
|gravity> ruby euler_array_inject1.rb | tail -1
7.6937453936572 -6.27772005661599 0.0 0.812206830641815 -0.574200201239989 0.0
Same answer as before. So yes, it works.
12.7. Shorter and Shorter
|gravity> irb
a, b, c = [10, "cat", 3.14]
[10, "cat", 3.14]
a
10
b
"cat"
c
3.14
quit
Dan: Oh, and before that, in the print_pos_vel method, you've
gotten rid of a line as well. What does flatten do?
|gravity> irb
[1, [[2, 3], [4,5], 6], 7].flatten
[1, 2, 3, 4, 5, 6, 7]
quit
Dan: And then just before the last print statement, you combine two
statements into one, using a semicolon. Four little tricks, saving us
four lines. I'm impressed!
12.8. Enough
|gravity> ruby euler_array_inject2.rb | tail -1
I guess not. But how can it produce nothing?
|gravity> ruby euler_array_inject2.rb | tail -3
7.68562253804505 -6.27197741210993 0.0 0.81228556121432 -0.5742644506056 0.0
7.6937453936572 -6.27772005661599 0.0 0.812206830641815 -0.574200201239989 0.0
Ah, of course, I've been a bit too clever. By adding the
return character \n character to the same line in
the printing method, I have caused an extra two blank spaces
to appear in the end. Well, I can get rid of that simply
by reversing the order, by printing the blank spaces first.
Here is euler_array_inject3.rb:
|gravity> ruby euler_array_inject3.rb | tail -3
7.677498893594 -6.26623412376799 0.0 0.812364445105053 -0.574328834193899 0.0
7.68562253804505 -6.27197741210993 0.0 0.81228556121432 -0.5742644506056 0.0
7.6937453936572 -6.27772005661599 0.0 0.812206830641815 -0.574200201239989 0.0
Same!
Previous | ToC | Up | Next |