In order to provide a working knowledge of ATOM, it is recommended that
you follow along with the design of the examples (which are in the
amc/examples/atom directory.
One of the things I like most about programming computers is that here is this machine with no preconceptions or surprises where you can craft any kind of world you want. So I will show you how to craft a very basic world with AMC.
The source file for our sample is called basic.d and
it resides in amc/examples/atom/basic/source. Like
any other AMC module, basic.d begins with a
rather standard module header:
module type atom;
Because we want to use the ATOM extensions defined in the AMC compiler,
we define the type of this module as atom. Next, we are going
to define a few handy things that will be useful for the snippets of
C we will be writing.
interface <-
/* Define a type name for a zero-terminated string. */
typedef char *String;
end;
The way the ATOM syntax in AMC is programmed, each data type that ATOM must deal with should be specified with a single name. This is partly due to my refusal to put C declaration parsing in AMC and partly because this is a convention that ATOM tries to enfore (ATOM is more than a library, it is a set of very formal conventions).
We begin by defining some messages. Although the message
keyword can have a very complex syntax for now, we will use a very
simple version:
message(HappyBirthday) message(Graduate)
Here we define two commands that we can send to objects. Internally,
they are actually represented as unsigned int constants.
However, the AMC compiler takes care of assigning unique identifiers
for you. You should also notice that there is no semicolon separating
statements.
Now we will define our first type, a person. Of course, our simple
Person objects are not nearly as clever as real people (well,
most people), they only know their name and their age. Of course,
since we want our people objects to do things, we will make them
respond to the messages we defined earlier.
object_type(Person)
{
data(String) { name }
data(int) { age }
method(HappyBirthday) <-
self->age++;
printf("%s is %d year%sold today.\n",
self->name,
self->age,
"s " + (self->age == 1));
end;
order_data { name, age }
}
The first thing we do is define the data members of the object. A few
exceptions aside, the order of the statements inside the
object_type block does not matter. One exception is the
order_data keyword that we will discuss shortly.
The data keyword defines a set of variables that are stored
within the object of a certain type. In our example, we define two
data members, name and age. As you may have guessed,
name is of the type String that we defined above in
the interface section and age is of type int.
If we omit the parenthesis and the type name, the data
statement defines members of type Object which you will find out
as you proceed through this tutorial is a pointer to any kind of object.
Next we indicate that objects of type Person will be handling
the HappyBirthday message via the method keyword.
Within the code associated with the message (which is termed the
``method'') the keyword self is defined as a pointer to the
object. All of the data members defined with the data
keyword are accessible (as well as some others) via this pointer.
As with most C in AMC, a new keyword, end marks the
completion of the block of code.
The last statement we use in our type definition is order_data.
This keyword simply defines the order of the various data members so that
objects of this type can be easily initialized statically. If this keyword
is not specified that actual order of the variables in memory is undefined.
Of course, we can't be content with such a simple object. Instead of defining a completely new type of object now, let us define one that extends the behavior of a Person by adding a new property and support for a new message. In some object-oriented programming systems, this is called ``inheritance.''
Since a great deal of our lives is spent suffering in school, let us
remember some of those times by making students do more work than
regular people and accept the message Graduate. Of course,
we want all of the behavior that People objects have without
having to know what that behavior is. In order to do this, we first
must put all of the properties of a Person within a student.
The ATOM compiler declares the members (and some additional control
information that will become apparent later) in a structure and
(automatically) gives that structure a type name that is the same as
the object types name suffixed with the word Data.
object_type(Student)
{
data(PersonData) { human }
data(int) { grade }
As you can see, we have added a single new data member, grade,
in addition to all the data members defined in a Person. Now we
can proceed to handle the Graduate message as we would any other
message in an object type.
method(Graduate) <-
char *suffix;
switch (++(self->grade))
{
case 1: suffix = "st"; break;
case 2: suffix = "nd"; break;
case 3: suffix = "rd"; break;
default: suffix = "th"; break;
}
printf("%s is in the %d%s grade.\n",
self->human.name,
self->grade,
suffix);
end;
The problem that remains is how to funnel all of the messages we
are not handling into the human member. The ATOM compiler
provides a keyword, otherwise that is a ``catch all''
for all unhandled messages. Within the ATOM header files, there is
a macro that is called Deliver that sends a message to
an object.
Just as the self keyword points to the object receiving
the message within methods, the msg keyword points to the
message that was sent. Therefore, to funnel all of these messages
into we simply do the following:
otherwise <-
/* Send all other messages to the human. */
return (Deliver(&self->human, msg));
end;
The return statement is necessary because the Deliver
primitive returns true if the message was handled or
false if no handler could be found for the message. Normally the
AMC compiler codes the returns for you, but in the case of an
otherwise block it may be necessary to report that the message
coult not be handled.
And of course, just as with Person objects, we specify the
order of the data member for easy initialization:
order_data { human, grade }
}
If we just kept going forward we would run into a little problem: Our people would get very lonely (and being lonley sucks). Therefore, let us make them a pet to keep them company. I happen to like cats, so we will give them a kitty cat (actually, the reason for the kitty cat will become obvious a little bit later).
object_type(Cat)
{
data(int) { age }
method(HappyBirthday) <-
self->age++;
printf("Meow. I'm a cat. I'm %d years old, but for a human "
"that's %d years! Meow.\n",
self->age,
self->age * 5); /* 1 cat year ~= 5 human years. */
end;
}
It is important to understand that at this point we have told the computer what people ``look like'' and ``what they do'' but we have not made any people yet.
One of the big differences between regular software and object-oriented software (that always seems to be glossed over in most texts) is that object oriented software does not assume you always have one of kind.
For example, a display driver written in a non-object oriented fasion might include a bunch of procedures to access the display (``abstraction'') as well as a few variables that are represent buffers or display memory. The problem with this is that the driver works great as long as the assumption that you have one display per program holds true. As soon as this breaks down you have to start talking about which display you are talking about.
Object oriented stresses always wrapping up all data so that when an operation is performed on it the requestor of the operation (not the operation its self) manages the data. And being rather object-oriented, ATOM follows this philosophy.
While we will eventually define simpler ways of allocating objects, for
now we can be content with just declaring them as variables (which is
why we used the order_data keyword). Because this demo program
is so simplistic, we will just define our objects as global variables:
implementation <-
PersonData joe = { &PersonType, "Joe", 25 };
PersonData jim = { &PersonType, "Jim", 31 };
StudentData bob = {
&StudentType,
{ &PersonType, "Bob", 6 },
3
};
CatData fluffy = { &CatType, 1 };
The first thing that stands out of place is the PersonType
stuff. The reason for this is that each object must know its own type
(this is necessary when sending messages to objects). In addition, the
type must always come first. Other than that, we just initialize the
fields in the order that we specify.
For the student, we have to initialize the human within him as well; and the correct initialization of objects must be performed recursively for the entire structure.
Now comes the fun part. We have worked very hard to define our little world but up to this point it has been rather static. But the work was worth it; now all that must be done is for us to send down some requests.
As you may have guessed, requests are messages; and messages are actually objects. Because messages are constructed and sent so frequently the ATOM compiler makes a shortcut for sending messages (you don't have to explicitly declare and initialize the message).
int main(void)
{
SendHappyBirthday(&joe);
SendHappyBirthday(&jim);
SendHappyBirthday(&bob);
SendHappyBirthday(&fluffy);
SendGraduate(&bob);
return (0);
}
end; /* The implementation section. */
The ATOM compiler generates ``delivery functions'' that construct and
send off the message. These are named with a prefix of Send
followed by the name of the message (similar to the way the Data
and Type names are made).
Now we can start to see what ATOM can do. The code in main sends off
four HappyBirthday messages and one Graduate message.
We don't care (or even really want to know) that one of the objects we
are asking to have a HappyBirthday is a cat. Even though the
code for having a birthday for a cat is wildly different from that of
a person, the code in main does not have to make this
decision.
This is most commonly referred to as ``polymorphism'' and it refers to the ability of a single piece of code to handle a (potentially) infinite number of data types.