Help / Learning / Ch 25: OOP

From J Wiki
Jump to navigation Jump to search


>> << Pri JfC LJ Phr Dic Voc !: Rel NuVoc wd Help Learning J


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'

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:

                o =: conew 'Class'
                create__o arg

can be abbreviated as:

                o =: 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|ctag|j|jadetag|jcompare|jregex|jtask|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'
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 IP (created automatically) and our methods which we defined for 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:

  1. change the current locale to '0'. Now the fields of object S, (that is, the the items variable of locale '0') are available.

  2. 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.
  3. 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|
+-+-----+
   

In conclusion, note particularly that an object is a locale.

This means that an object is not a variable or a value.  

An object is not something which can be an argument to a function, or returned by a function. An object cannot be part of another object.

All these things can be done using names of objects in place of objects. Every object has a unique system-generated name, represented by a string. This string is just an ordinary value, and all the usual things can be done with it.

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'
   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 coinsert.

   coclass 'Set'
   coinsert 'Collection'

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
coinsert '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


The examples in this chapter were executed using J version 802 beta. This chapter last updated 30 Jun 2014
Copyright © Roger Stokes 2014. This material may be freely reproduced, provided that acknowledgement is made.


>> << Pri JfC LJ Phr Dic Voc !: Rel NuVoc wd Help Learning J