Previous ToC Up Next

12. Array Methods

12.1. An Array Declaration

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:

 a = []               

Let me write the new version in a new file, euler_array.rb, in the hope things will be correct now:

 include Math
 
 r = [1, 0, 0]
 v = [0, 0.5, 0]
 a = []               
 dt = 0.01
 
 print(r[0], "  ", r[1], "  ", r[2], "  ")
 print(v[0], "  ", v[1], "  ", v[2], "\n")
 
 1000.times{
   r2 = r[0]*r[0] + r[1]*r[1] + r[2]*r[2]
   r3 = r2 * sqrt(r2)
   a[0] = - r[0] / r3
   a[1] = - r[1] / r3
   a[2] = - r[2] / r3
   r[0] += v[0]*dt
   r[1] += v[1]*dt
   r[2] += v[2]*dt
   v[0] += a[0]*dt
   v[1] += a[1]*dt
   v[2] += a[2]*dt
   print(r[0], "  ", r[1], "  ", r[2], "  ")
   print(v[0], "  ", v[1], "  ", v[2], "\n")
 }

12.2. Three Array Methods

Dan: Seeing is believing. Does it now work?

Carol: Let's try:

 |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.

Carol: Well, let's check:

 |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.

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:

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

Erica: That looks nice and compact.

Dan: Does it work?

Carol: Let's see:

 |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

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

 r.each{|x| print(x, "  ")}               

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:

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

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.

 |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.

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.

12.4. The map Method

Dan: What about this mysterious map that you are using in line:

   a = r.map{|x| -x/r3}                   

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:

 |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.

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

   a = r.map{|x| -x/r3}                   

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.

12.5. Defining a Method

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:

 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}
   r.each_index{|k| r[k] += v[k]*dt}
   v.each_index{|k| v[k] += a[k]*dt}
   print_pos_vel(r,v)
 }

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:

 |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

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:

 |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.

Carol: Indeed. So this will allow me to make the loop part of the code a bit shorter, in euler_array_inject1.rb:

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

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:

 |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

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:

 |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?

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:

 |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!

Carol: Ah, but I can do better! How about this one, euler_array_inject2.rb?

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

12.8. Enough

Dan: Two lines less. You're getting devious! And does it work?

 |gravity> ruby euler_array_inject2.rb | tail -1
   
I guess not. But how can it produce nothing?

Carol: Beats me. Strange. Let's show a bit more output:

 |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:

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

and here are the results:

 |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!

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!
Previous ToC Up Next