Ch 25: OOP

| # Chapter 25: Object-Oriented Programming ## 25.1 Background and Terminology In this chapter "OOP" will stand for "object-oriented programming". Here is the barest thumbnail sketch of OOP. On occasion, a program needs to build, maintain and use a collection of related data, where it is natural to consider the collection to be, in some sense, a whole. For example, a "stack" is a sequence of data items, such that the most-recently added item is the first to be removed. If we intend to make much use of stacks, then it might be a worthwhile investment to write some functions dedicated to building and using stacks. The combination of some data and some dedicated functions is called an object. Every object belongs to some specific class of similar objects. We will say that a stack is an object of the `Stack` class. The dedicated functions for objects of a given class are called the "methods" of the class. For example, for objects of the `Stack` class we will need a method for adding a new item, and a method for retrieving the last-added item. An object needs one or more variables to represent its data. Such variables are called fields. Thus for a stack we may choose to have a single field, a list of items. In summary, OOP consists of identifying a useful class of objects, and then defining the class by defining methods and fields, and then using the methods. By organizing a program into the definitions of different classes, OOP can be viewed as a way of managing complexity. The simple examples which follow are meant to illustrate the machinery of the OOP approach, but not to provide much by way of motivation for OOP. We will be using a number of library functions, mostly with names beginning "co", meaning "class and object". A brief summary of them is given at the end of this chapter. ## 25.2 Defining a Class ### 25.2.1 Introducing the Class For a simple example, we look at defining a class which we choose to call "Stack". A new class is introduced with the library function `coclass`.
       coclass 'Stack'
    +-----+-+
    |Stack|z|
    +-----+-+
`coclass` is used for its effect, not its result. The effect of `coclass` is to establish and make current a new locale called `Stack`. To verify this, we can inspect the name of the current locale:
       coname ''
    +-----+
    |Stack|
    +-----+
### 25.2.2 Defining the Methods A new object comes into being in two steps. The first step uses library verb `conew` to create a rudimentary object, devoid of fields, a mere placeholder. The second step gives a new object its structure and initial content by creating and assigning values to the field-variables. We will deal with the first step below. The second step we look at now. It is done by a method conventionally called `create` (meaning "create fields", not "create object"). This is the first of the methods we must define. For example, we decide that a `Stack` object is to have a single field called `items`, initially an empty list.
       create =: 3 : 'items =: 0 $ 0'
The connection between this method and the `Stack` class is that `create` has just been defined in the current locale, which is `Stack`. This `create` method is a verb. In this example, it ignores its argument, and its result is of no interest: it is executed purely for its effect. Its effect will be that the (implicitly specified) object will be set up to have a single field called `items` as an empty list. Our second method is for pushing a new value on to the front of the `items` in a stack.
       push =: 3 : '# items =: (< y.) , items'
The `push` method is a verb. Its argument `y.` is the new value to be pushed. We made a design-decision here that `y.` is to be boxed and then pushed. The result is of no interest, but there must be some result, so we chose to return `(# items)` rather than just `items`. Next, a method for inspecting the "top" (most-recently added) item on the stack. It returns that value of that item. The stack is unchanged.
       top =: 3 : '> {. items'
Next a method to remove the top item of the stack.
       pop =: 3 : '#  items =: }. items'
Finally, a method to "destroy" a `Stack` object, that is, eliminate it when we are finished with it. For this purpose there is a library function `codestroy`.
       destroy =: codestroy
This completes the definition of the `Stack` class. Since we are still within the scope of the `coclass 'Stack'` statement above, the current locale is `Stack`. To use this class definition we return to our regular working environment, the `base` locale.
       cocurrent 'base'
## 25.3 Making New Objects Now we are in a position to create and use `Stack` objects. A new `Stack` is created in two steps. The first step uses the library verb `conew`.
       S =: conew 'Stack'
The result of `conew` which we assigned to `S` is not the newly-created object itself. Rather, the value of `S` is in effect a unique reference-number which identifies the newly-created `Stack` object. For brevity we will say "Stack `S`" to mean the object referred to by `S`. Stack `S` now exists but its state is so far undefined. Therefore the second step in making the object is to use the `create` method to change the state of `S` to be an empty stack. Since `create` ignores its argument, we supply an argument of `0`
       create__S 0
Now we can push values onto the stack `S` and retrieve them in last-in-first-out order. In the following, the expression (`push__S 'hello'` means: the method `push` with argument `'hello'` applied to object `S`.
       push__S 'hello'
    1
       push__S 'how are you?'
    2
       push__S 'goodbye'
    3
       pop__S 0
    2
       top__S 0
    how are you?
### 25.3.1 Dyadic Conew The two steps involved in creating a new object, `conew` followed by `create`, can be collapsed into one using dyadic `conew`. The scheme is that:
                C =: conew 'Class'
                create__C arg
can be abbreviated as:
                C =: arg conew 'Class'
That is, any left argument of `conew` is passed to `create`, which is automatically invoked. In this simple `Stack` class, `create` ignores its argument, but even so one step is neater than two. For example:
       T =: 0 conew 'Stack'
       push__T 77
    1
       push__T 88
    2
       top__T 0
    88
## 25.4 Listing the Classes and Objects In this section we look at inspecting the population of objects and classes we have created. The expression `(18!:1) 0 1` produces a list of all existing locales.
       (18!:1) 0 1
    +-+-+-----+----+-+-+
    |0|1|Stack|base|j|z|
    +-+-+-----+----+-+-+
We see here the names of locales of 3 different kinds. Firstly, there are ordinary locales such as `base`, and `z`, described in Chapter 24. These are created automatically by the J system. Depending on the version of J you are using, you may see a list different from the one shown here. Secondly, there are locales such as `Stack`. The `Stack` locale defines the `Stack` class. If we view this locale (with the `view` utility function from Chapter 24)
       view 'Stack'
    COCLASSPATH =: <;._1 ' Stack z'
    IP          =: 1
    create      =: 3 : 'items =: 0 $ 0'
    destroy     =: codestroy
    pop         =: 3 : '#  items =: }. items'
    push        =: 3 : '# items =: (< y.) , items'
    top         =: 3 : '> {. items'
we see a variable `COCLASSPATH`, which identifies this locale as a class-definition, and the methods of `Stack`. Thirdly, we have locales such as `0`. Here the name is a string of numeric digits (that is, `'0'`). Such a locale is an object. The variable `S` has the value `<'0'`, so that here object `S` is locale `'0'`.
`S` | `view >S`
    +-+
    |0|
    +-+
    COCREATOR =: <'base'
    items     =: <;._1 '|how are you?|hello'
We see a variable `COCREATOR`, which identifies this locale as an object, and the field(s) of the object. The path from an object is given by the verb `18!:2`
       18!:2 S
    +-----+-+
    |Stack|z|
    +-----+-+
Since `S` is a `Stack` object, the first locale on its path is `Stack`. Recall from Chapter 24 that, since `S = <'0'` then the expression `push__S 99` means: change the current locale to `'0'`. Now the fields of object `S`, (that is, the the `items` variable of locale `'0'`) are available. apply the `push` verb to argument `99`. Since `push` is not in locale `'0'`, a search is made along the path from locale `'0'` which takes us to locale `Stack` whence `push` is retrieved before it is applied. Restore the current locale to the status quo. Here is a utility function to list all the existing objects and their classes.
       obcl =: 3 : '(, ({. @: (18!:2)))"0  (18!:1) 1'
Currently we have variables `S` and `T` each referring to a `Stack` object.
`S` | `T` | `obcl ''`
    +-+
    |0|
    +-+
    +-+
    |1|
    +-+
    +-+-----+
    |0|Stack|
    +-+-----+
    |1|Stack|
    +-+-----+
(Again, depending on the version of J you are using, you may see further objects and classes automatically generated by the J system for its own use.) A Stack, `S` say, can be removed using the `destroy` method of the `Stack` class.
       destroy__S  ''
    1
We see it has gone.
       obcl ''
    +-+-----+
    |1|Stack|
    +-+-----+
## 25.5 Inheritance Here we look at how a new class can build on an existing class. The main idea is that, given some class, we can develop a new class as a specialized version of the old class. For example, suppose there is a class called `Collection` where the objects are collections of values. We could define a new class where, say, the objects are collections without duplicates, and this class could be called `Set`. Then a `Set` object is a special kind of a `Collection` object. In such a case we say that the `Set` class is a child of the parent class `Collection`. The child will inherit the methods of the parent, perhaps modifying some and perhaps adding new methods, to realize the special properties of child objects. For a simple example we begin with a parent-class called `Collection`,
       coclass 'Collection'
    +----------+-+
    |Collection|z|
    +----------+-+
       create  =: 3 : 'items =: 0 $ 0'
       add     =: 3 : '# items =: (< y.) , items'
       remove  =: 3 : '# items =: items -. < y.'
       inspect =: 3 : 'items'
       destroy =: codestroy
Here the `inspect` method yields a boxed list of all the members of the collection. A quick demonstration:
       cocurrent 'base'
       C1 =: 0 conew 'Collection'
       add__C1 'foo'
    1
       add__C1 37
    2
       remove__C1 'foo'
    1
       inspect__C1 0
    +--+
    |37|
    +--+
Now we define the `Set` class, specifying that `Set` is to be a child of `Collection` with the library verb `coextend`.
       coclass 'Set'
    +---+-+
    |Set|z|
    +---+-+
       coextend 'Collection'
    +---+----------+-+
    |Set|Collection|z|
    +---+----------+-+
To express the property that a `Set` has no duplicates, we need to modify only the `add` method. Here is something that will work:
       add =: 3 : '# items =: ~. (< y.) , items'
All the other methods needed for `Set` are already available, inherited from the parent class `Collection`. We have finished the definition of `Set` and are ready to use it.
       cocurrent 'base'
       s1 =: 0 conew 'Set'  NB. make new Set object.
       add__s1 'a'
    1
       add__s1 'b'
    2
       add__s1 'a'
    2
       remove__s1 'b'
    1
       inspect__s1 0        NB. should have just one 'a'
    +-+
    |a|
    +-+
### 25.5.1 A Matter of Principle Recall the definition of the `add` method of class `Set`.
       add_Set_
    3 : '# items =: ~. (< y.) , items'
It has an objectionable feature: in writing it we used our knowledge of the internals of a `Collection` object, namely that there is a field called `items` which is a boxed list. Now the methods of `Collection` are supposed to be adequate for all handling of `Collection` objects. As a matter of principle, if we stick to the methods and avoid rummaging around in the internals, we hope to shield ourselves, to some degree, from possible future changes to the internals of `Collection`. Such changes might be, for example, for improved performance. Let's try redefining `add` again, this time sticking to the methods of the parent as much as possible. We use our knowledge that the parent `inspect` method yields a boxed list of the membership. If the argument `y.` is not among the membership, then we add it with the parent `add` method.
       add_Set_ =: 3 : 0
    if. (< y.) e. inspect 0
    do.  0
    else. add_Collection_ f. y.   NB. see below !
    end.
    )
Not so nice, but that's the price we pay for having principles. Trying it out on the set `s1`:
       inspect__s1 0
    +-+
    |a|
    +-+
       add__s1     'a'
    0
       add__s1     'z'
    2
       inspect__s1 0
    +-+-+
    |z|a|
    +-+-+
## 25.6 Using Inherited Methods Let us review the definition of the `add` method of class `Set`.
       add_Set_
    3 : 0
    if. (< y.) e. inspect 0
    do.  0
    else. add_Collection_ f. y.   NB. see below !
    end.
    )
There are some questions to be answered. ### 25.6.1 First Question How are methods inherited? In other words, why is the `inspect` method of the parent `Collection` class available as a `Set` method? In short, the method is found along the path, that is, a `Set` object such as `s1` is a locale. It contains the field-variable(s) of the object. when a method of a class is executed, the current locale is (temporarily) the locale of an object of that class. This follows from the way we invoke the method, with an expression of the form `method__object argument`. the path from an object-locale goes to the class locale and thence to any parent locale(s). Hence the method is found along the path. . We see that a `Set` object such as `s1` has a path to `Set` and then to `Collection`.
       copath > s1
    +---+----------+-+
    |Set|Collection|z|
    +---+----------+-+
### 25.6.2 Second Question In the definition of `add_Set_`
       add_Set_
    3 : 0
    if. (< y.) e. inspect 0
    do.  0
    else. add_Collection_ f. y.   NB. see below !
    end.
    )
Given that the parent method `inspect` is referred to as simply `inspect`, why is the parent method `add` referred to as `add_Collection_`? Because we are defining a method to be called `add` and inside it a reference to `add` would be a fatal circularity. ### 25.6.3 Third Question why is the parent `add` method specified as `add_Collection_ f.` ? Because `add_Collection_` is a locative name, and evaluating expressions with locative names will involve a change of locale. Recall from Chapter 24 that `add_Collection_ 0` would be evaluated in locale `Collection`, which would be incorrect: we need to be in the object locale when applying the method. Since `f.` is built-in, by the time we have finished evaluating `(add_Collection_ f.)` we are back in the right locale with a fully-evaluated value for the function which we can apply without change of locale.
       add_Collection_ f.
    3 : '# items =: (< y.) , items'
## 25.7 Library Verbs Here is a brief summary of selected library verbs.
`coclass 'foo'` | introduce new class `foo`
`coextend 'foo'` | this class to be a child of `foo`
`conew 'foo'` | introduce a new object of class `foo`
`conl 0` | list locale names
`conl 1` | list ids of object locales
`names_foo_ ''` | list the methods of class `foo`
`copath <'foo'` | show path of class `foo`
`coname ''` | show name of current locale
This brings us to the end of Chapter 25

NEXT
Table of Contents
Index

Copyright © Roger Stokes 2004. This material may be freely reproduced, provided that this copyright notice is also reproduced.

last updated 27 Oct 2004