1.2. A Body Class
Bob: For example, a single particle has as a minimum a mass,
a position and velocity. Whenever you deal with a particle, you
would like to have all three variables at hand. You can't put them in
a single array, because mass is a scalar, and the other two variables
are vectors, so you have to come up with a more general form of
bundling.
The basic idea of this kind of programming is called object-oriented
programming. In many older computer languages, you can pass variables
around, where each variable can contain a number or an array of numbers;
or you may pass pointers to such variables or arrays. In an
object-oriented programming language, you pass bundles of information
around: for example all the information pertaining to a single
particle, or even to a whole N-body system. This provides convenient
handles on the information.
If you look in a computer science book, you will read that the
glorious reason for object-oriented programming is the ability to make
your life arbitrarily difficult by hiding any and all information
within those objects, but I don't particularly care for that aspect.
Alice: I do think encapsulation has its good sides, but we can come
back to that later. What exactly is it that you can put inside an object?
I guess an object can contain internal variables. Can it contain
functions as well?
Bob: To take the specific case of Ruby, a typical class contains both.
For a class to be useful, at least you have to be able to create an
instance of a class, so you need something like what is called a
constructor in C++. In the case of Ruby, like in the case of C++, you
have the freedom to define an initializer, through which you can
create an instance of a class with your desired values for the
internal variable. Or you can choose not to define an initializer,
that is fine too.
Note as a matter of terminology that what I have called a function, or
what would be called a subroutine in Fortran, is called a method in
Ruby. There are other objects in Ruby, besides classes. Sometimes
you have a group of functions that are either similar or just work
together well, and you may want to pass them around as a bundle. In
Ruby, such bundles of functions are called modules. But to get
started, it is easier to stick to classes for now.
Alice: Ah, that is nice! Does this mean that we can define an
integration algorithm as a module, independent of the particular
variables in the classes that define a body or an N-body system? I
mean, can you write a leapfrog module that can propagate particles,
independently of their type? You could have point particles in either
a two-dimensional of a three-dimensional world. Or you could have
particles with a finite radius, that stick together when they collide;
as long as they are not too close, they could be propagated by the
same leapfrog module.
Bob: You really have an interesting way of approaching a problem.
We haven't even defined a single particle, and you are already
thinking about general modules that are particle independent. I
suggest we first implement a particle. I think this is how we
can introduce a minimal class for a single particle:
class Body
def initialize(mass = 0, pos = [0,0,0], vel = [0,0,0])
@mass, @pos, @vel = mass, pos, vel
end
end
In a few minutes, we can go through the precise meaning of these
constructs, but here is the general idea: you can give three arguments
in a call to initialize, to specify the mass, position, and velocity.
If you don't specify some of these arguments, they will acquire the
default values that are given here by all the zeroes in the first line
of the definition of initialize. The next line assigns these values
to internal variables that have names starting with a @ sign.
Alice: That is remarkably short and simple! In fact, it seems too
simple. I am surprised that we do not have to declare the internal
variables. In other languages that I am familiar with, it is
essential that you tell the computer which memory places to set aside
before naming them.
Bob: Ruby is dynamically typed. This means that the type of a
variable is determined at run time. In other words the type of a
variable is simply the type of the value that is assigned to a
variable.
Alice: And you can change the type of that value, whenever you want?
Can we try that? I'd like to see the syntax of how you do that.
Bob: Before we do that, just one thing: staring at this amazingly
simple class definition makes me realize both how similar it is to
what you would write in C++, and how different.
Alice: Indeed, the logical structure of C++ class definitions
is very similar.
Bob: But the big difference is that a C++ class definition
is quite a bit longer.
Alice: I'm curious how much longer. Do you remember how to write a
similar particle class in C++?
Bob: That shouldn't be too hard. Always easiest to look at an
existing code. Ah, here I have another C++ code that I wrote a while
ago. Okay, now I remember. Of course. Here is how you do it in
C++:
##include <iostream>
using namespace std;
class body
{
private:
double pos[3];
double vel[3];
double mass;
public:
body(double inmass, double inpos[3], double invel[3]){
mass = inmass;
for(int i=0;i<3;i++){
pos[i] = inpos[i];
vel[i] = invel[i];
}
}
void print(){
cout << mass <<endl;
cout << pos[0] <<" "<< pos[1] <<" "<< pos[2] <<endl;
cout << vel[0] <<" "<< vel[1] <<" "<< vel[2] <<endl;
}
};
main()
{
double zero3[3]={0.0,0.0,0.0};
body x = body(0.0,zero3,zero3);
x.print();
}
1.3. The irb Interpreter
Alice: That is quite an impressive difference, between Ruby and C++!
But aren't you cheating a bit? That last part, with the main function
down below, does not occur in your short and sweet Ruby class definition.
Bob: It doesn't occur there, because you don't need it. All that
main does for you is create an object and then printing its internal
values. You can let Ruby do that for you without even asking for it.
Here is what you do. The easiest way to work with Ruby is to use the
command irb. The acronym stands for interactive Ruby. You
invoke it by simply typing irb on the command line. Now as soon as
you create an instance of a class in Ruby, the interpreter echoes the
content to you, for free!
Alice: I'll try it out. But rather than wrestling with a whole
particle, I prefer to start with a single variable, to see how Ruby
functions at its most basic level. Also, remember that I asked you
whether you can change the type of the value of a variable, whenever
you want? I want to see that for myself. One thing at a time!
Let me introduce an identifier id. I will first give it a numerical
value, and then I will assign to it a string of characters, to give it
a name. Since Ruby is friendly enough not to insist on declaring my
variables beforehand, I presume I can just go ahead and use id right
away.
|gravity> irb
irb(main):001:0> id = 12
=> 12
And as you predicted, a value gets produced magically. But where does
that come from?
Bob: Just like in C, variables are not the only things that have values.
In fact, every expression has a value. And irb makes life more
clear by echoing the value of each line as soon as you enter it.
Alice: I like that. It will make debugging a lot easier. Okay, let
me try to change the type of id.
irb(main):002:0> id = cat
NameError: undefined local variable or method `cat' for main
:Object
from (irb):2
Bob: Ah, Ruby treats your cat in an equally friendly way as
your id, assuming it is itself a name of a variable (or a method),
rather than a content that can be assigned to a variable.
Alice: But that line works fine when I write shell scripts. Since Ruby
is called a scripting language, I thought it might work here too.
Bob: Each scripting language has different conventions. In your shell
case, I bet you have to invoke the value of a variable cat by typing
$cat, each time you use it. Ruby has another solution: typing
cat echoes the value of cat. When you want to introduce a string
consisting of the three letters c, a, and t, you type "cat".
Alice: Here goes:
irb(main):003:0> id = "cat"
=> "cat"
It worked! And presumably id has now forgotten that it ever was a
numerical variable.
Bob: Indeed. And at any time you can ask Ruby what the type of
your dynamically typed variable currently is. Any variable is an
instance of some class. And the class it belongs to in turn has a
method built in, not surprisingly called class, which tells you the
type of that class. In Ruby, you invoke a method associated with a
variable by writing that variable followed by a period and the method
name.
Alice: I find it surprising, if you ask me; I would have expected
something like type. But I'll take your word for it.
irb(main):004:0> id.class
=> String
irb(main):005:0> id = 12
=> 12
irb(main):006:0> id.class
=> Fixnum
Hey, that is nice! You can immediately check what is going on.
Let's see what happens when I type in the text of the Body class
declaration above.
irb(main):007:0> class Body
irb(main):008:1> def initialize(mass = 0, pos = [0,0,0], vel = [0,0,0])
irb(main):009:2> @mass, @pos, @vel = mass, pos, vel
irb(main):010:2> end
irb(main):011:1> end
=> nil
I see another nice feature. I had been wondering about the meaning of
the :0 after each line number. That must have been the level
of nesting of each expression. It goes up by one, each time you enter
a block of text that ends with end.
Bob: And since you only give the definition of Body without yet
creating any of its instances, there is no value associated with it.
Here nil means effectively `undefined'.
Alice: But we have just defined the Body class; why does Ruby
claim it is undefined?
Bob: In Ruby, the definition of a class does not return a value,
since there is no reasonable answer that can be given to a non-existent
question. But since the interpreter wants to echo something, it just
returns, `nil' as meaning an undefined value, or literally nothing.
And this has nothing to do with the definition of a class. This may
sound more complicated than it is, but it is quite logical.
Alice: I see, yes, that makes sense. So the class Body has only
one function, starting with def and ending with the inner end,
correct?
Bob: Indeed. And the last end is the end of the class definition,
that starts with class Body. Note the grammatical rule that
the name of a class such as Body always starts with a capital
letter. The names of normal variables, in contrast, start with a
lower case letter: we have three such variables, mass, pos, and
vel. All three are given here as possible parameters to the
initialization function initialize.