Topic: APLX Help : Help on APL language : APL Fundamentals : Classes and Objects
[ Previous | Next | Contents | Index | APL Home ]

www.microapl.co.uk

Classes and Objects


Overview of Classes and Objects

As well as traditional APL functions and operators, APLX adds object-oriented programming facilities to the core APL language. These facilities are broadly similar to those implemented in other object-oriented programming languages (such as C++, C#, Java, or Ruby), but with the difference that APL's array-programming approach applies to classes and objects in the same way as it applies to ordinary data.

The fundamental building block for object-oriented programming in APLX Version 4 is the class. For example, in a commercial invoicing application, a given class might represent the attributes and behavior of an Invoice, and another class might represent a CreditNote. In an application concerned with geometry, a class might represent a Sphere, or a Rectangle, or a Polygon. A class contains definitions both for program logic (functions and operators, known collectively as the methods of the class), and for data (named variables associated with the class, known as properties). The term members is used to describe both the properties and methods of a class.

In most cases, when you come to use a class, you need to create an instance of that class, also known as an object. Whereas the class represents an abstraction of (say) an Invoice, or a Sphere, or a Rectangle, an object represents a particular invoice, sphere or rectangle. Typically, you may have many instances of a given class, each containing independent copies of data (properties), but all supporting the same program logic (methods).

Inheritance

When you define a class, you can specify that it inherits from another class. The new class is said to be the child, and the class it inherits from is the parent or base class. Inheritance means that (unless you explicitly change their definition), all of the properties and methods defined in the parent class are also available in the child class. This works for further levels of inheritance as well, so that methods and properties can be inherited from the immediate parent, or from the parent's parent, and so on. The terms derived classes or descendants are sometimes used to denote the children of a class, and the children's children, and so on. Similarly, the term ancestors of a class is used to denote the parent, parent's parent, and so on.

For example, you might have a class Shape, representing an abstract geometric shape. This might have properties called 'X' and 'Y' giving the center point of the shape, and methods called 'Move' and 'Area'.

A Circle class might inherit from Shape, introducing further properties such as 'radius'. Equally, a class Polygon might also inherit from Shape, and further classes Triangle and Square inherit from Polygon. All of the classes Circle, Polygon, Triangle and Square are derived from Shape. Because of the way inheritance works, they would all include the properties X and Y, and the methods Move and Area.

When a class inherits from another, you can specify that the definition of a given method of the parent (or the initial value of a property) is different in the child class. In our example, you would need to supply a different definition of the Area method for a Circle and a Square. This is known as overriding the method.

For classes defined in APLX, all methods can be overridden, and all methods are virtual, that is to say if method A in a base class calls another method B, and the second method B is overridden in a child class, then running method A in the child class will cause the overridden version of B to be called, not the version of B defined in the parent.

APLX uses an inheritance model known as single inheritance. This means that a child class can be derived from only one parent (which may itself derive from another class, and so on). However, APLX also allows you to 'mix-in' one or more other classes (including external classes, such as those written in .Net or Java) into your objects at runtime. This is a very flexible feature which can be used in much the same way as multiple inheritance is used in some other languages. See the section on Mixins for more details.

User-defined, System and External classes

APLX supports the following types of class:

  • User-defined classes, written in APL (also known as 'Internal' or just 'APL' classes)
  • System classes, which are built-in to the APLX interpreter in the same way as System functions. System classes are currently used mainly for user-interface programming, and replace the older ⎕WI syntax.
  • External classes, written in other languages, such as Java or C#.

Object References and Class References

When you create an object, i.e. an instance of a class (using the system function ⎕NEW as described below), the explicit result that is returned is not the object itself, but a reference to the object. This reference is held internally as just a number, an index into a table of objects which APLX maintains in the workspace. If you assign the reference to another variable, the object itself is not copied; instead, you have two references to the same object.

Of course, because APLX is an array language, you can have arrays of object references, and you can embed object references in nested arrays along with other data. For example, you might have an array containing references to hundreds of Rectangle objects.

You can also have a reference to a Class. This makes it possible for general functions to act on classes without knowing in advance which class applies.

Creating objects (instances of classes)

The system function ⎕NEW is the principal means by which you create an object, i.e. an instance of a class. The class can be either written in APL (an internal or user-defined class), or a built-in System class, or a class written in an external environment such as .Net, Java or Ruby (an external class). ⎕NEW creates a new instance of the class, runs any constructor defined for the class, and returns a reference to the new object as its explicit result.

The class is specified as the right argument (or first element of the right argument). It can be specified either as a class reference, or as a class name (i.e. a character vector). Any parameters to be passed to the constructor of the class (the method which is run automatically when a class is created) follow the class name or reference.

If you specify the class by name, you also need to identify in the left argument the environment where the class exists, unless it is internal.

Creating instances of internal (user-defined) classes

Normally, you create an instance of a user-defined class by passing the class reference directly as the right argument (or first element of the right argument). For example, if you have a class called Invoice, you can create an instance of it by entering:

     I←⎕NEW Invoice

What is really happening here is that the symbol Invoice refers to the class definition, and when it is used in this way, it returns a reference to the class.

Note that you can also pass the class name rather than a class reference. The following are alternative ways of creating an instance of a user-defined class:

      I←⎕NEW 'Invoice'
      I←'apl' ⎕NEW 'Invoice'

Passing arguments to the constructor

A constructor is a special method of a class, which is run automatically when the class is created using ⎕NEW, and is used to initialize the class. For APL classes, the constructor is a method whose name is the same as the name of the class. It should be a function which takes a right argument, and does not return a result. (It can be a method which takes no argument, if you are sure that no parameters will ever be passed to it via ⎕NEW). Any arguments to the constructor can be provided as extra elements on the right argument of ⎕NEW. When the constructor is run, these extra elements are passed as the right argument to the constructor. If there are no extra elements, an empty vector is passed as the right argument to the constructor.

For example, suppose the class Invoice looks like this:

Invoice {
  TimeStamp
  Account
  InvNumber
  {Serial←0}

 ∇Invoice B
  ⍝ Constructor for class Invoice.  B is the account number
  Account←B
  TimeStamp←⎕TS
  Serial←Serial+1
  InvNumber←Serial
 ∇
}

This is a class which has a constructor and four properties. One of the properties (Serial) is a class-wide property, which means it has only a single value shared between all instances of the class. When a new instance of this class is created, the constructor will be run. It will store the account number (passed as an argument to ⎕NEW) in the property Account, and store the current time stamp in the property TimeStamp. It will then increment the class-wide property Serial (common to all instances of this class), and store the result in the property InvNumber. (To see the properties, we use the system method ⎕DS which summarizes the property values):

      S←⎕NEW Invoice 23533
      S.⎕DS
Account=23533, TimeStamp=2007 10 11 15 47 34 848, InvNumber=1
      T←⎕NEW Invoice 67544
      T.⎕DS
Account=67544, TimeStamp=2007 10 11 15 48 11 773, InvNumber=2

Default display of a class or object reference

When you call the ⎕NEW system function to create an object (an instance of a class), the explicit result is a reference to that object. The question therefore arises: what happens if you display such an object reference?

By default, APLX displays an object reference as the unqualified class name contained in square brackets. Class references are displayed as the class name in curly braces:

      )CLASSES
Queue                     ⍝ User-defined APL class
      Queue
{Queue}                   ⍝ Default display of class reference
      QUEUE23←⎕NEW Queue 
      QUEUE23             ⍝ Default display of APL object reference
[Queue]

However, if the APL programmer wishes to override the default display form of an object, this can easily be done by using the ⎕DF system method (see the section on system methods below):

      QUEUE23.⎕DF 'Checkout Queue23'

      QUEUE23
Checkout Queue23

Object references and object lifetimes

When you use ⎕NEW to create a new object, that object persists until there are no more references to it in the workspace. It is then deleted immediately, if it is an internal or system object. If it is an external object, such as an instance of a .Net class, the fact that there are no more references to it in the APL workspace means that it available for deletion by the external environment (unless the external environment itself has further references to the same object). However, in typical external environments such as .Net, Java and Ruby, the actual deletion of the object may not occur until later.

Consider this sequence, where we create an instance of a class called Philosopher which has a property Name:

      A←⎕NEW Philosopher
      A.Name←'Aristotle'

At this point, we have created a new instance of the class, and we have a single reference to it, in the variable A. We now copy the reference (not the object itself) to a variable B:

      B←A
      B.Name
Aristotle

We now have two references to the same object. So if we change a property of the object, the change is visible through either reference - they refer to the same thing:

      B.Name←'Socrates'
      A.Name
Socrates

Now we erase one of the references:

      )ERASE A

We still have a second reference to the object. The object will persist until we delete the last reference to it:

      B.Name
Socrates
      )ERASE B

At this point, there are no more references to the object left in the workspace, and the object itself is deleted.

It follows from this that, if you use ⎕NEW to create an object, and do not assign the result to a variable, it will immediately be deleted again. In this example, we create an instance of the class Philosopher. The explicit result of ⎕NEW is a temporary workspace entry (of type object reference), which is displayed using the default display format for objects, and then deleted. At that point the object itself is also deleted, as there are no references left:

     ⎕NEW Philosopher
[Philosopher]

The Null object

As its name implies, the Null object is a special case of an object, which has no properties and no methods of its own (although System methods may apply to it). A reference to the Null object displays in the special form:

[NULL OBJECT]

A reference to the Null object can arise for a number of different reasons:

  • If you have an array of object references, the prototype of the array is a reference to the Null object. For example:

          VEC←⎕NEW ¨Rectangle Sphere Triangle
          VEC
    [Rectangle] [Sphere] [Triangle]
          1↑3↓VEC
    [NULL OBJECT]
  • An external call may return a Null object, for example if you are looping through a linked list of objects and reach the last one.

  • An APLX System method may return a Null object, for example if you ask for the parent class of a top-level class:

         Point.⎕PARENT
    [NULL OBJECT]
  • Your application code can deliberately set an object reference to Null (by calling ⎕NULL), for example to indicate that it has not yet been initialized.

  • APLX may be forced to set an object reference to Null, because it is no longer valid. For example, this will happen if you )SAVE a workspace which contains a reference to an external object (e.g. a Java or .NET object). On re-loading the workspace at a later date, the object reference is no longer valid since the external object no longer exists.

Types of Property

When you define a class, you specify the names of the properties of that class, which can be used to hold data associated with the class. You can optionally specify a default value for the property, that is the value which the property will have in a newly-created instance of the class. You can also specify that the property is read-only, which means it is not possible to assign a new value to it.

Most properties are instance properties, which means that each instance of the class has a separate copy of the property (for example, the X and Y position of a Shape). Occasionally, however, it is useful to define a class-wide property (known in some other languages as a static or shared property). This is a property where there is a single copy of the data, shared between all instances. This is useful for cases such as keeping a unique incrementing serial number (the next invoice number, for example).

Combining these concepts, you have the following main types of property:

  • A read-write instance property, with a default value specified in the class definition
  • A read-write instance property, with no default value specified in the class definition
  • A read-write class-wide property, with a default value specified in the class definition
  • A read-write class-wide property, with no default value specified in the class definition
  • A read-only class-wide property, with a default value specified in the class definition

You can also in principle have a read-only property with no initial value, but this is not very useful! You can also have a read-only instance property, but this is indistinguishable from a read-only class-wide property because you can't assign a different value to it in different instances.

Implementation note: APLX uses a 'create-on-write' approach when you assign to an instance property. This means that, if you have never changed the value of a property for a particular instance since the instance was first created, the value which is returned when you read the property is the default value stored in the class definition. It follows that, if you change the class definition so that the property has a different default value, the change will immediately be reflected in all instances of the class, unless the property has been modified for that instance.

Name scope, and Public versus Private members

The members of a class (i.e its properties and methods) can be either public or private. Public members can be accessed from outside the class, whereas private members can only be accessed from within methods defined in the class (or from desk calculator mode, if a method has been interrupted because of an error or interrupt and the method is on the )SI stack). Private members can also be accessed by methods defined in a child (derived) class. If you are familiar with other object-oriented languages such as C++ or Visual Basic, this means that private methods in APLX correspond to 'protected' methods in those languages.

If you want to access a public member of an object from outside the class (i.e. not within a method of the class), then you use dot notation to refer to it. This takes the form ObjectReference.MemberName. For example, suppose you have a variable myrect which is a reference to an object of class Rectangle. You could call the Move method and access the X and Y properties for that object as follows:

      myrect.X←45
      myrect.Y←78
      myrect.Move 17 6
      myrect.X
62
      myrect.Y
84

Within the methods of the class itself, you do not normally need to use dot notation. This is because the search order for symbols encountered when executing a method is as follows:

  1. First, APLX looks to see if the symbol refers to a member defined in the class of the object.
  2. If not, it looks to see if the member is defined in the parent class (if any), iterating through each of the ancestors in turn.
  3. If it is not found in any of the ancestors, it then looks in the local variables of the method.
  4. Finally, it looks in the global symbol table.

Thus, a simple implementation of the Move method above (defined in the Shape class from which Rectangle derives) might be something like this:

   ∇ Move B
[1]  ⍝ Move shape by amount B specified as change to X, Y
[2]  (X Y)←(X,Y)+B
   ∇

Constructors

As we saw earlier, a constructor is a special type of method, which is run automatically when an instance of a class is created using ⎕NEW. It can be used to initialize the object, optionally using parameters passed to ⎕NEW. For example, you might use this mechanism to specify the initial position of a Rectangle object.

For a user-defined class, a constructor is defined as a method which takes a right argument, and which has the same name as the class itself.

In some other object-oriented programming languages, constructors are a very important part of the language because they are the only way of initializing property values. For user-defined classes in APLX, default values can be set up in the class definition, so constructors are not always needed.

Where a class inherits from another class, the constructor which gets run automatically is that of the class itself (if it has a constructor), or of the first ancestor class which has a constructor. Normally, in a constructor, you will want to do some initialization specific to the class itself, and also call the constructor of the parent class (using ⎕PARENT) to do any initialization which it and its ancestors require. You can do this at any point in the constructor; there is no restriction on where you make this call to the parent's constructor; indeed, you don't have to call it at all if it is not appropriate.

In APLX, a constructor is also a perfectly ordinary method; it can be called in the normal way by one of the other methods in the class, or from outside (if it declared as Public). This can be useful for re-initializing an object.

Some object-oriented languages also include a special method called a destructor, which is called just before the object is deleted. APLX user-defined classes do not have destructors. This means that, if you need to release system resources (for example, close a file or a database connection), you need to call a method to do that explicitly before erasing the last reference to the internal object. However, APLX will automatically take care of deleting all the properties of the object, and releasing the memory back to the workspace.

Using Classes without Instances

So far, we have concentrated on using objects as instances of classes. However, classes can also be very useful in their own right, without the need to make instances of them. There are two major reasons why you might want to define a class which can be used directly:

Defining a set of constants

If you define a class with a set of read-only properties, those properties can be used as a set of constant values or 'enumerations'. For example, you might have a class called Messages, which holds all the messages which your application displays to the user:

Messages {
OutOfMemory←←'There is not enough memory to continue'
AskModelName←←'Enter the name of the model'
OpComplete←←'Operation Complete'
AskReset←←'Do you want to reset the model?'
...etc
}

You can then use this class in your application (without having to make an instance of it) to encapsulate all the messages and refer to them by name:

      ∇R←CheckWS
[1]   :If R←⎕WA<MIN_FREE_WS
[2]     ShowError Messages.OutOfMemory
[3]   :EndIf
      ∇

This keeps all the messages together in one place, allows you to refer to them by a name which is easy to remember and is self-documenting, but does not pollute the global symbol space with hundreds of APL variables.

Keeping namespaces tidy

In traditional APL systems, it often used to be the case the number of global functions was very large. By placing related functions in a class, the workspace can be kept tidy.

For example, in a statistical application, you might have a class Average which contained methods for calculating many different types of average (Mean, Median, Mode etc). As long as these methods do not write to any property of the class, there is no need to make an instance of the class to run them; you can just run them using dot notation as Average.Mean, Average.Median etc.

Note that, in APLX classes, there is no pre-determined difference between a method which can only be run inside an instance (sometimes known as an instance method), and a method which can be run as a class member without an instance being created (sometimes known as a static method). The only difference is that, at run time, if a method writes to a property, an error will be generated if there is no instance to write to.

However, you do need to be aware of the difference between static and instance methods when using classes written in other languages such as Java or C#. See the system function ⎕CALL for more details.

Editing User-Defined Classes

You can create and edit user-defined classes in a number of ways:

  • Using the on-screen class editor, invoked from the Edit menu or )EDIT. The class editor allows you to edit each method of the class, as well as set up properties and default values;

  • Using the line ('del') editor;

  • Using the system function ⎕FX, to convert a text representation into a class;

  • Using the system function ⎕IC, to transfer global functions, operators and variables into the class as methods and properties.


Topic: APLX Help : Help on APL language : APL Fundamentals : Classes and Objects
[ Previous | Next | Contents | Index | APL Home ]