3.3. Taming the Arrays
Bob: the M word again! At some point you have to define what modular
exactly means. If you call every good idea that works `modular', you
have a tautology. Anyway, I'm curious whether it will come out better
now, when we type print.
|gravity> irb --prompt short_prompt -r body3.rb
001:0> b1 = Body.new(0.1, [1.3, 0, 0], [0, 0.5, 0])
#<Body:0x400d0cf4 @mass=0.1, @vel=[0, 0.5, 0], @pos=[1.3, 0, 0]>
002:0> print b1
mass = 0.1
pos = 1.300
vel = 00.50
nil
Alice: It looks nicer all right, but what happened to poor pos
and vel? The mass came out fine though.
Bob: Ah, of course, look, the elements of each array are all
concatenated: [1.3, 0, 0] has become 1.300.
Remember what I said about strings? In that case addition means
concatenation. So all the characters of the array elements are
strung together.
Alice: That must be why they call it a string! But how do we
separate the fields?
Bob: Guess what, we use a field separator. The book tells me
that we can define our own version. Here it is: the method join
converts an array to a string, and you can give a separator as an
argument. Let's try. Here is body4.rb:
class Body
attr_accessor :mass, :pos, :vel
def initialize(mass = 0, pos = [0,0,0], vel = [0,0,0])
@mass, @pos, @vel = mass, pos, vel
end
def to_s
" mass = " + @mass.to_s + "\n" +
" pos = " + @pos.join(", ") + "\n" +
" vel = " + @vel.join(", ") + "\n"
end
end
I'll do the same exercise:
|gravity> irb --prompt short_prompt -r body4.rb
001:0> b1 = Body.new(0.1, [1.3, 0, 0], [0, 0.5, 0])
#<Body:0x400d0c7c @mass=0.1, @vel=[0, 0.5, 0], @pos=[1.3, 0, 0]>
002:0> print b1
mass = 0.1
pos = 1.3, 0, 0
vel = 0, 0.5, 0
nil
Alice: Well done! But I don't understand why pos and vel suddenly
have acquired this nifty method join, which in C++ would be called
a member function. Who gave that to them?
Bob: The manual says that join is an instance method of the class
Array. And since pos and vel are each arrays, they both have
access to the join method. It was given to them the moment they
were created as arrays. So whenever you create an array, you immediately
have a whole arsenal of useful methods right at your finger tips. Look,
isn't this an impressive list of methods?
Alice: I guess so, but let's see how useful they will turn out to be.
You mentioned that join is an instance method. What would be an example
of a class method?
Bob: Here they are listed, in the Ruby documentation pages, under the
heading class Array. There are only two: [] and new.
Alice: We have seen new; you told me that that is a generic method for
any class. But what is []?
Bob: Typing Array.new gives you an empty array, to which you can
add elements afterwards. Typing Array[](1,a,3) gives you an array
that is already initialized with some elements, as specified in the arguments
of this method; in this case it will return the array [1, a, 3].
Alice: I see. If you look at it that way, it makes sense. The only thing
different is that there is no dot between Array and [].
Bob: However, it would still be a bit cumbersome to initialize an array
by having to type the expression Array[] each time. There are two
better alternatives. You can simply type Array[1,a,3], which
gives you the exact same thing. Or you can even type [1,a,3], leaving
out the word Array altogether, and you still get [1, a, 3].
So the class method [] is a versatile and somewhat slippery thing.
Yet it really is a class method, and the various ways of invoking it are
again forms of syntactic sugar, making your life a lot easier.
Alice: Ruby must be a really sweet language! I guess I'd better
study the manual. There must be a lot of tools as well as notations,
ready to be used.
3.4. Executing Ruby Files
Bob: So now our Body class has grown up: it can be printed out
correctly with print.
Alice: I wonder though, can't we give our Body its own pretty
printing method, pp say? In such a way that we can type
b1.pp, and it will give us this nicely formatted output?
Bob: Easy. Here is body5.rb. Note that I put a comment
in, since otherwise I would never remember what pp meant.
class Body
attr_accessor :mass, :pos, :vel
def initialize(mass = 0, pos = [0,0,0], vel = [0,0,0])
@mass, @pos, @vel = mass, pos, vel
end
def to_s
" mass = " + @mass.to_s + "\n" +
" pos = " + @pos.join(", ") + "\n" +
" vel = " + @vel.join(", ") + "\n"
end
def pp # pretty print
print to_s
end
end
Alice: Ah, that's because you are not a Lisp programmer. But you
are right. That was very easy. Let's check.
|gravity> irb --prompt short_prompt -r body5.rb
001:0> b1 = Body.new(0.1, [1.3, 0, 0], [0, 0.5, 0])
#<Body:0x400d0ad8 @mass=0.1, @vel=[0, 0.5, 0], @pos=[1.3, 0, 0]>
002:0> b1.pp
mass = 0.1
pos = 1.3, 0, 0
vel = 0, 0.5, 0
nil
And it works! But I'm getting pretty tired from retyping that whole
initialization line for our single body. An interactive interpreter
is fine for small jobs, but perhaps it makes more sense to start
putting both the class definition and the commands all in one file.
We can then keep that file in an editor, and each time we modify
either the class or the command, we can just run the file.
Bob: That's an excellent idea. In fact, I was getting tired of
creating that whole series of files from body1.rb up to
body5.rb. Let us just take one file and call it simply
test.rb. Here is the first version, and from now on we
can add and change whatever we want. You see, at the end I have
included our two commands, the first to create and initialize a
particular particle, and the second to print it out.
class Body
attr_accessor :mass, :pos, :vel
def initialize(mass = 0, pos = [0,0,0], vel = [0,0,0])
@mass, @pos, @vel = mass, pos, vel
end
def to_s
" mass = " + @mass.to_s + "\n" +
" pos = " + @pos.join(", ") + "\n" +
" vel = " + @vel.join(", ") + "\n"
end
def pp # pretty print
print to_s
end
end
b1 = Body.new(0.1, [1.3, 0, 0], [0, 0.5, 0])
b1.pp
Alice: But how do we run this file, in order to execute those
two commands that you added at the end?
Bob: The simplest way is to type ruby followed by the
file name:
|gravity> ruby test.rb
mass = 0.1
pos = 1.3, 0, 0
vel = 0, 0.5, 0
Alice: Much easier indeed! No more retyping. And no more nil
at the end either. Ah, of course. The interactive interpreter echoes
the value of each line that you type. But when you execute a file,
you only get the results of each command that you give.
Bob: Yes, that's the difference, and that's why this looks cleaner.
Of course, for debugging purposes you may want to have an echo of
all that passes in front of you, but I'm happy to work with a file,
for the time being.