Next Previous Contents

4. Examples

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.

4.1 A basic introduction.

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 module header

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

Defining the messages

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.

Defining the object types

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;          
  }

Making instances of the types we defined

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.

Sending messages

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.


Next Previous Contents