Creating and Using Classes and Methods

You can define a class using either directives or messages.

To define a class using directives, you place a ::CLASS directive after the main part of your source program:

::class "Account"

This creates an Account class that is a subclass of the Object class. Object is the default superclass if one is not specified. (See The Object Class for a description of the Object class.) The string "Account" is a string identifier for the new class. The string identifier is both the internal class name and the name of the environment symbol used to locate your new class instance.

Now you can use ::METHOD directives to add methods to your new class. The ::METHOD directives must immediately follow the ::CLASS directive that creates the class.

::method type
  return "an account"

::method "name="
  expose name
  use arg name

::method name
  expose name
  return name

This adds the methods TYPE, NAME, and NAME= to the Account class.

You can create a subclass of the Account class and define a method for it:

::class "Savings" subclass account
::method type
return "a savings account"

Now you can create an instance of the Savings class with the NEW method (see NEW) and send TYPE, NAME, and NAME= messages to that instance:

asav = .savings~new
say asav~type
asav~name = "John Smith"

The Account class methods NAME and NAME= create a pair of access methods to the account object variable NAME. The following directive sequence creates the NAME and NAME= methods:

::method "name="
  expose name
  use arg name

::method name
  expose name
  return name

You can replace this with a single ::ATTRIBUTE directive. For example, the directive

::attribute name

adds two methods, NAME and NAME= to a class. These methods perform the same function as the NAME and NAME= methods in the original example. The NAME method returns the current value of the object variable NAME; the NAME= method assigns a new value to the object variable NAME.

In addition to defining operational methods and attribute methods, you can add "constant" methods to a class using the ::CONSTANT directive. The ::CONSTANT directive will create both a class method and an instance method to the class definition. The constant method will always return the same constant value, and can be invoked by sending a message to either the class or an instance method. For example, you might add the following constant to your Account class:

::constant checkingMinimum 200

This value can be retrieved using either of the following methods

  say .Account~checkingMinimum    -- displays "200"
  asave = .savings~new
  say asave~checkingMinimum       -- also displays "200"

Using Classes

When you create a new class, it is always a subclass of an existing class. You can create new classes with the ::CLASS directive or by sending the SUBCLASS or MIXINCLASS message to an existing class. If you specify neither the SUBCLASS nor the MIXINCLASS option on the ::CLASS directive, the superclass for the new class is the Object class, and it is not a mixin class.

Example of creating a new class using a message:

persistence = .object~mixinclass("Persistence")
myarray=.array~subclass("myarray")~~inherit(persistence)

Example of creating a new class using the directive:

::class persistence mixinclass object
::class myarray subclass array inherit persistence

Scope

A scope refers to the methods and object variables defined for a single class (not including the superclasses). Only methods defined in a particular scope can access the object variables within that scope. This means that object variables in a subclass can have the same names as object variables used by a superclass, because the variables are created at different scopes.

Defining Instance Methods with SETMETHOD or ENHANCED

In Rexx, methods are usually associated with instances using classes, but it is also possible to add methods directly to an instance using the SETMETHOD (see SETMETHOD) or ENHANCED (see ENHANCED) method.

All subclasses of the Object class inherit SETMETHOD. You can use SETMETHOD to create one-off objects, objects that must be absolutely unique so that a class that is capable of creating other instances is not necessary. The Class class also provides an ENHANCED method that lets you create new instances of a class with additional methods. The methods and the object variables defined on an object with SETMETHOD or ENHANCED form a separate scope, like the scopes the class hierarchy defines.

Method Names

A method name can be any string. When an object receives a message, the language processor searches for a method whose name matches the message name in uppercase.

Note: The language processor also translates the specified name of all methods added to objects into uppercase characters.

You must surround a method name with quotation marks when it contains characters that are not allowed in a symbol (for example, the operator characters). The following example creates a new class (the Cost class), defines a new method (%), creates an instance of the Cost class (mycost), and sends a % message to mycost:

cost=.object~subclass("A cost")
cost~define("%", 'expose p; say "Enter a price."; pull p; say p*1.07;')
mycost=cost~new
mycost~"%"        /* Produces:  Enter a price.             */
                  /* If the user specifies a price of 100, */
                  /* produces: 107.00                      */

Default Search Order for Method Selection

The search order for a method name matching the message is for:

  1. A method the object itself defines with SETMETHOD or ENHANCED. (See SETMETHOD .)

  2. A method the object's class defines. (Note that an object acquires the instance methods of the class to which it belongs at the time of its creation. If a class gains additional methods, objects created before the definition of these methods do not acquire these methods.)

  3. A method that a superclass of the object's class defines. This is also limited to methods that were available when the object was created. The order of the INHERIT (see INHERIT) messages sent to an object's class determines the search order of the superclass method definitions.

This search order places methods of a class before methods of its superclasses so that a class can supplement or override inherited methods.

If the language processor does not find a match for the message name, the language processor checks the object for a method name UNKNOWN. If it exists, the language processor calls the UNKNOWN method and returns as the message result any result the UNKNOWN method returns. The UNKNOWN method arguments are the original message name and a Rexx array containing the original message arguments.

If the object does not have an UNKNOWN method, the language processor raises a NOMETHOD condition. If there are no active traps for the NOMETHOD condition, a syntax error is raised.

Defining an UNKNOWN Method

When an object that receives a message does not have a matching message name, the language processor checks if the object has a method named UNKNOWN. If the object has an UNKNOWN method, the language processor calls UNKNOWN, passing two arguments. The first argument is the name of the method that was not located. The second argument is an array containing the arguments passed with the original message.

For example, the following UNKNOWN method will print out the name of the invoked method and then invoke the same method on another object. This can be used track the messages that are sent to an object:

::method unknown
expose target      -- will receive all of the messages
use arg name, arguments
say name "invoked with" arguments~toString
forward to(target)   -- send along the message with the original args

Changing the Search Order for Methods

You can change the usual search order for methods by:

  1. Ensuring that the receiver object is the sender object. (You usually do this by specifying the special variable SELF.

  2. Specifying a colon and a class symbol after the message name. The class symbol can be a variable name or an environment symbol. It identifies the class object to be used as the starting point for the method search.

    The class object must be a superclass of the class defining the active method, or, if you used SETMETHOD to define the active method, the object's own class. The class symbol is usually the special variable SUPER (see SUPER) but it can be any environment symbol or variable name whose value is a valid class.

Suppose you create an Account class that is a subclass of the Object class, define a TYPE method for the Account class, and create the Savings class that is a subclass of Account. You could define a TYPE method for the Savings class as follows:

savings~define("TYPE", 'return "a savings account"')

You could change the search order by using the following line:

savings~define("TYPE", 'return self~type:super "(savings)"')

This changes the search order so that the language processor searches for the TYPE method first in the Account superclass (rather than in the Savings subclass). When you create an instance of the Savings class (asav) and send a TYPE message to asav:

say asav~type

an account (savings) is displayed. The TYPE method of the Savings class calls the TYPE method of the Account class, and adds the string (savings) to the results.

Public and Private Methods

A method can be public or private. Any object can send a message that runs a public method. A private method can only be invoked from specific calling contexts. These contexts are:

  1. From within a method owned by the same class as the target. This is frequently the same object, accessed via the special variable SELF. Private methods of an object can also be accessed from other instances of the same class (or subclass instances).

  2. From within a method defined at the same class scope as the method. For example:

    ::class Savings
    ::method newCheckingAccount CLASS
      instance = self~new
      instance~makeChecking
      return instance
    
    ::method makeChecking private
      expose checking
      checking = .true

    The newCheckingAccount CLASS method is able to invoke the makeChecking method because the scope of the makeChecking method is .Savings.

  3. From within an instance (or subclass instance) of a class to a private class method of its class. For example:

    ::class Savings
    ::method init class
      expose counter
      counter = 0
    
    ::method allocateAccountNumber private class
      expose counter
      counter = counter + 1
      return counter
    
    ::method init
      expose accountNumber
      accountNumber = self~class~allocateAccountNumber

    The instance init method of the Savings class is able to invoke the allocateAccountNumber private method of the .Savings class object because it is owned by an instance of the .Savings class.

Private methods include methods at different scopes within the same object. This allows superclasses to make methods available to their subclasses while hiding those methods from other objects. A private method is like an internal subroutine. It shields the internal information of an object to outsiders, but allowing objects to share information with each other and their defining classes.

Initialization

Any object requiring initialization at creation time must define an INIT method. If this method is defined, the class object runs the INIT method after the object is created. If an object has more than one INIT method (for example, it is defined in several classes), each INIT method must forward the INIT message up the hierarchy to complete the object's initialization.

Example:

asav = .savings~new(1000.00, 6.25)
say asav~type
asav~name = "John Smith"

::class Account

::method INIT
  expose balance
  use arg balance

::method TYPE
  return "an account"

::method name attribute

::class Savings subclass Account

::method INIT
  expose interest_rate
  use arg balance, interest_rate
  self~init:super(balance)

::method type
  return "a savings account"

The NEW method of the Savings class object creates a new Savings object and calls the INIT method of the new object. The INIT method arguments are the arguments specified on the NEW method. In the Savings INIT method, the line:

self~init:super(balance)

calls the INIT method of the Account class, using just the balance argument specified on the NEW message.

Object Destruction and Uninitialization

Object destruction is implicit. When an object is no longer in use, Rexx automatically reclaims its storage. If the object has allocated other system resources, you must release them at this time. (Rexx cannot release these resources, because it is unaware that the object has allocated them.)

Similarly, other uninitialization processing may be needed, for example, by a message object holding an unreported error. An object requiring uninitialization should define an UNINIT method. If this method is defined, Rexx runs it before reclaiming the object's storage. If an object has more than one UNINIT method (defined in several classes), each UNINIT method is responsible for sending the UNINIT method up the object hierarchy.

Required String Values

Rexx requires a string value in a number of contexts within instructions and built-in function calls.

If you supply an object other than a string in these contexts, by default the language processor converts it to some string representation and uses this. However, the programmer can cause the language processor to raise the NOSTRING condition when the supplied object does not have an equivalent string value.

To obtain a string value, the language processor sends a REQUEST("STRING") message to the object. Strings and other objects that have string values return the appropriate string value for Rexx to use. (This happens automatically for strings and for subclasses of the String class because they inherit a suitable MAKESTRING method from the String class.) For this mechanism to work correctly, you must provide a MAKESTRING method for any other objects with string values.

For other objects without string values (that is, without a MAKESTRING method), the action taken depends on the setting of the NOSTRING condition trap. If the NOSTRING condition is being trapped (see Conditions and Condition Traps), the language processor raises the NOSTRING condition. If the NOSTRING condition is not being trapped, the language processor sends a STRING message to the object to obtain its readable string representation (see the STRING method of the Object class STRING) and uses this string.

When comparing a string object with the Nil object, if the NOSTRING condition is being trapped, then

if string = .nil

will raise the NOSTRING condition, whereas

if .nil = string

will not as the Nil object's "=" method does not expect a string as an argument.

Example:

d = .directory~new
say substr(d,5,7)         /* Produces "rectory" from "a Directory" */
signal on nostring
say substr(d,5,7)         /* Raises the NOSTRING condition */
say substr(d~string,3,6)  /* Displays "Direct" */

For arguments to Rexx object methods, different rules apply. When a method expects a string as an argument, the argument object is sent the REQUEST("STRING") message. If REQUEST returns the Nil object, then the method raises an error.

Concurrency

Rexx supports concurrency, multiple methods running simultaneously on a single object. See Concurrency for a full description of concurrency.