Help / JforC / Modular Code
>> << Pri JfC LJ Phr Dic Voc !: Rel NuVoc wd Help J for C Programmers
29. Modular Code
Separating your code into independent modules boils down to segmenting the space of variable names into subspaces that overlap to the extent you want. We will discuss how J handles namespaces, and then see how this corresponds to the classes and objects provided by C++.
A reminder on nomenclature: private names are assigned with =. and are visible only to the entity they are assigned in; public names are visible to all entities.
Locales And Locatives
Every public named entity in J is a member of a single locale. Each locale has a name which is a list of characters. A locative is a name containing a simple name (the only kind of name we have encountered so far) and an explicit locale, in one of the two forms simplename_localename_ and simplename__var . In the form simplename_localename_, localename is the name of the explicit locale; in the form simplename__var, the variable var must be a scalar boxed string whose opened contents provide the name of the explicit locale. Examples:
abc_z_ is simple name abc and locale z
vv =. <'lname'
def__vv is simple name def and locale lname
Note that a simple name may contain an underscore; it may not end with an underscore or contain two underscores in a row.
(Note: J makes a distinction between named locales whose names are valid J variable names not including an underscore, and numbered locales whose names are strings representing nonnegative decimal integers with no leading zeroes. The difference between the two is small: numbered locales use slightly less memory, and numbered locales must be created in ascending numerical order. Numbered locales are created when you use conew to create an object, as we will discuss below).
The current locale is a value kept by J and used to influence the processing of names. We will learn what causes it to change. The current locale is the name of a locale. When J starts, the current locale is set to 'base' .
Assignment
An assignment is private if it is of the form simplename=.value and is executed while an explicit definition is running (an explicit definition is the result of the : conjunction, for example a verb defined by 3 : 'text' or verb define, or a modifier defined by conjunction define or adverb define). An entity assigned by private assignment is not part of any locale and is accessible only by sentences executed by the explicit definition that was running when the entity was assigned. The idea is this: there is a pushdown stack of namespaces in which private entities are assigned. When an explicit definition E is executed, it starts with a new empty namespace that will be destroyed when E finishes. Any private assignments made while E is running are made in this private namespace. Any private variables referred to by E or by tacitly-defined entities invoked by E are taken from this private namespace. If E invokes another explicit definition F, F starts off with its own private namespace and has no access to elements of E's private namespace. When F finishes, returning control to E, F's private namespace is destroyed. E is said to be suspended while F is running.
Assignments that are not private (because they assign to a locative, use =:, or are executed when no explicit definition is running) are public. Assignment to a locative creates an entity having the simple name in the locative, residing in the explicit locale given by the locative. A public assignment to a simple name creates the named entity in the current locale. Entities in a locale are not affected by completion of an explicit definition; they have a life of their own and can be referred to by any verb that knows how to reach the locale they are in. The following examples illustrate assignments; the interpretation given is correct if the lines are entered from the keyboard:
simp1 =. 5 NB. public (outside of explicit definition) vb1 =: verb define NB. public isimp =. simp1 NB. private, referring to public simp1 simp1 =. 8 NB. private (=. inside definition) loc1_z_ =. 10 NB. public (locative) simp2 =: 12 NB. public (=:) isimp,simp1 NB. result ) vb1 '' NB. execute vb1, see result 5 8
Note that simp1 was set to 8 by the explicit definition. Because this was a private assignment, the public value was not changed:
simp1 5
The public value is still 5, as it was before the explicit definition was executed.
loc1_z_ 10 simp2 12
The other public assignments leave their results in the locale they were assigned to.
isimp |value error: isimp
The entities assigned by private assignment were destroyed when vb1 finished.
Note that the load verb (which runs scripts) is an explicit definition. Any assignment using =. executed during load will be lost. Use =: to define names in scripts.
Referencing a Name: Scope
Names and locatives used to refer to entities look just like names appearing as targets of assignments, but reference is more complicated than assignment because referencing conducts a search through several namespaces in order.
First, the name is sought in the private namespace of the executing entity. If the name is found, its private value is used.
If the name is not found as a private name (because it has not been assigned as a private name, or there is no executing entity, or the name is a locative), it is sought as a public name in the locales, as explained in the next section.
Note that the same name can exist as a private name and a public name. It is not unusual or harmful for an entity to use a private name, blissfully unaware that the same name is public in some locale. The converse situation is not so benign: if an entity creates a private name and subsequently makes a public assignment to the name, as in
name =. 5 ... name =: 7
the odds are that this is a mistake, because any reference to name is going to use the private version. The interpreter treats this case, where a global assignment is made to a name already defined locally, as a domain error. If you really need to perform the assignment (perhaps you want to have the value available for inspection after the function finishes), assign to a locative rather than a simple name: name__ =: or name_locale_ =: .
Referencing a Public Name: The Search Path
Each locale has a search path (usually called simply the path) which is a list of boxed locale names. The path is set and queried by the foreign 18!:2 which goes by the alias copath . Examples:
('loc1';'loc2';'z') 18!:2 <'loc3'
Sets the path for locale 'loc3' to 'loc1' followed by 'loc2' (and the obligatory 'z').
copath <'loc3' +----+----+-+ |loc1|loc2|z| +----+----+-+
Queries the path for 'loc3' .
Every reference to a public name implicitly uses a path. A reference to a locative looks for the simple name in the locative's explicit locale; if the name is not found there, the locales in the path of the explicit locale are examined one by one until the simple name is found (if the name is not found in any locale in the path, it is an undefined name). A reference to a simple name that was not found as a private name is similar, starting in the current locale and continuing if necessary in the locales in the current locale's path; in other words, a simple name that is not found as a private name is treated as if it were written simplename_currentlocale_ .
Note that only the path of the starting locale (either the current locale or the explicit locale) specifies the search order. No other paths are used.
Examples of references:
('loc1';'loc2';'z') 18!:2 <'loc3' a_loc1_ =: 'a' a_loc2_ =: 'b' c_loc3_ =: 'c' c_loc2_ =: 'd' a_loc3_ a
The name was not defined in 'loc3' so the path was used, and the name was found in 'loc1' .
a_loc2_ b
The value in 'loc2' can be retrieved if we start the search there.
c_loc3_ c
If the value is found in the starting locale, no search is performed.
c_loc1_ |value error: c_loc1_
We have not defined a path for 'loc1', so 'loc2' is not searched.
Changing The Current Locale
The current locale can be changed in two ways: explicitly by executing the cocurrent verb, and implicitly by executing a verb named by a locative.
cocurrent y sets the current locale to y . Simple as that. cocurrent uses the foreign 18!:4 . Do not use 18!:4 directly! It is intended to be used under an alias, and it has side effects.
Executing an entity named by a locative (almost always a verb, but it could be a modifier as well) saves the current locale, changes the current locale to the explicit locale of the locative before starting the entity, and resets the current locale to the saved value when the entity finishes. Note that the entity always runs in the explicit locale of the locative, even if the search for the name found the entity in some other locale in the search path.
Whenever a named entity finishes execution, the locale is restored to its original value, even if the entity changed the current locale.
Here is an extended example of actions affecting the current locale: We load a utility file:
load 'printf'
We establish the search paths we want, and display the name of the current locale:
('loc1';'loc2';'z') 18!:2 <'loc3' ('loc2';'z') 18!:2 <'loc1' 18!:5 '' +----+ |base| +----+
Next we define two verbs.
v1_z_ =: verb define 'Locale at start of v1 is %j' printf 18!:5 '' qprintf 'n1 ' v2_result =. v2_loc1_ n1 'Value returned by v2 is %s' printf <v2_result 'Locale in v1 after calling v2 is %j' printf 18!:5 '' qprintf 'n1 ' ) cocurrent <'loc2' v2 =: verb define 'Locale at start of v2 is %j' printf 18!:5 '' qprintf 'n1 y ' cocurrent <'loc2' qprintf 'n1 ' 'Locale at end of v2 is %j' printf 18!:5 '' n1 )
The verb v1 was defined in locale 'z' because it was an assignment to a locative; the verb v2 was defined in locale 'loc2' because it was an assignment to a simple name and the current locale at the time of its assignment was 'loc2' .
cocurrent <'loc3' v2 =: [:
Now the verb v2 is defined, with different values, in locales 'loc2' and 'loc3' . Next we define the noun n1 in each of our locales, so we can see which locale a name was found in:
n1_loc1_ =: 'n1 in loc1' n1_loc2_ =: 'n1 in loc2' n1_loc3_ =: 'n1 in loc3'
Now run the verbs. I will insert interpretation of the execution.
v1 ''
J searches for the simple name v1 in the current locale 'loc3'; not finding it there it looks in 'loc1', 'loc2', and 'z', finally finding it in 'z' . J executes the definition of the verb found in 'z', but without changing the current locale.
Locale at start of v1 is loc3
Yes, the current locale is still 'loc3' ...
n1=n1 in loc3
...and a name lookup uses the current locale as the starting point.
Locale at start of v2 is loc1
v1 has executed v2_loc1_ . J starts searching for the name v2 in locale 'loc1' and its path, eventually finding it in 'z' . v2 is executed, using the definition found in 'z', but with the current locale set to the explicit locale of the locative, namely 'loc1' . Note that v2 was also defined in 'loc3' (as [: which would give an error), but 'loc3' was never searched. The operand of v2 was n1; note that the lookup for n1 is completely independent of the lookup for v2; n1 is sought and found in 'loc3' and it is that value that becomes y at the start of execution of v2 .
n1=n1 in loc1 y=n1 in loc3
Simple name lookups start in the current locale 'loc1' . The private name y has the value it was given on entry.
n1=n1 in loc2
Here we have switched the current locale to 'loc2' using cocurrent, and the name is found in the new current locale.
Locale at end of v2 is loc2 Value returned by v2 is n1 in loc2
Here v2 has finished and control has returned to v1 . Note that the value returned by v2 is simply the result of the last sentence executed; it is a noun. Here it is the value of n1 at the end of v2, at which time the current locale was 'loc2' .
Locale in v1 after calling v2 is loc3
Note that when v2 finished the current locale was restored to its value before execution of v2 . The cocurrent in v2 has no effect once v2 finishes.
n1=n1 in loc3
Execution of the verb v1 is complete. We should be back in locale 'loc3' from which we executed v1 :
18!:5 '' +----+ |loc3| +----+
True Globals--The Shared Locale 'z'
It is a J convention, universally adhered to, that every locale's search path must end with the locale 'z' . Any name in the 'z' locale can then be referred to by a simple name from any locale, making names in the 'z' locale truly global names.
Using Locales--Object-Oriented Programming
You have my sympathy for having to read through that detailed description of name processing; I refuse to apologize, though, because you really can't write programs if you don't know what names mean. But, you wonder, How do I use locales?
Object-Oriented Programming in J: Base Classes
You won't go far wrong to think of a locale as akin to a class in C++. When you have a set of functions that go together as a module, define all their verbs and nouns in a single locale. The easiest way to do this is to put a line
coclass <'localename'
at the beginning of each source file for the module (coclass is like cocurrent but it supports inheritance). Then, every public assignment in the file will automatically be made in the locale localename .
The simplest kind of module, comprising a set of routines and some private data storage, is called a base class. A typical base class is shown in Figure 1. It is a class to manage an execution log. Programs make calls to addlog to append information to the log. A call to setmodule saves a module name to be appended to each log item. A call to readlog returns the most recent log entries.
Locale specifier |
coclass 'execlog' |
Initializations |
currmod =: '' |
One-time |
'loglist' InitIfUndef 0$a: |
Methods |
setmodule =: monad : 'currmod =: y' addlog =: verb define loglist =: loglist , < currmod,y ) readlog =: verb define y =. {. y , 10 (-y <. #loglist) {. loglist ) |
Public interface |
setmodule_z_ =: setmodule_execlog_ addlog_z_ =: addlog_execlog_ readlog_z_ =: readlog_execlog_ |
Figure 1. Sample class definition: execution checkpoint logging |
The first line of the module establishes the locale for the class. Next come the initializations, which are normally just public assignments that initialize the nouns used by the class.
The one-time initializations are an optional element used mainly during debugging. If you are debugging code and you reload the script that defines a class, all the initializations will be performed again. This may destroy values that have been assigned by calls to the class. To preserve existing definitions, use the InitIfUndef adverb defined in jforc.ijs. If the name has been defined already, it is not changed, but if it is not defined it is given an initial value.
The methods are the verbs that do the work of the class. These include both verbs that can be called by users of the class and verbs used only by the class itself.
The last component is the public interface for the class. The names defined in the locale are the equivalent of the private portion of class. To provide the public interface to the class, you need to put those names in a place where they can be found by other modules. The traditional way to do this is to define them in the locale 'z' by ending each file with lines like
epname_z_ =: epname_localename_
Here epname is the name of a verb, and localename is the name of the module's locale. Take a minute to see what happens when some other locale invokes epname . The name search for epname will end in the locale 'z' where this definition is found. Execution of this definition immediately results in execution of epname_localename_ which switches the current locale to localename and runs the definition of epname found there. The benefit is that the calling module doesn't need to know the locale that epname is going to be executed in.
If you tire of writing out the public definitions one by one, I have included in jforc.ijs a verb to do it for a list of entry points using a sentence like
PublishEntryPoints 'setmodule addlog readlog'
Object-Oriented Programming in J: Derived Classes
A derived class is a class that has all the features of a base class, plus a few additions of its own. It is easy to create such a class in J. In Figure 2 we create a timestamping class that adds timestamps to the log entries provided by the execlog class defined above:
Locale specifier |
coclass 'timelog' |
Append base class |
coinsert 'execlog' |
Initializations |
addtimelog =: verb define addlog y,' ',": 6!:0 '' ) |
Public interface |
addtimelog_z_ =: addtimelog_timelog_ |
Figure 2. Derived class: timestamp logging |
We use coinsert to give the timelog class all the facilities of the execlog class. A call to addtimelog will add a timestamped log entry, while calls to addlog, readlog, and setmodule are handled as before.
Object-Oriented Programming in J: Objects
As described so far, a class is a collection of code with associated private data storage. The same entities can be thought of as a set of data with associated methods for operating on the data; this view can be extended to allow multiple instances of the data, each instance sharing the same set of methods for operating on the instance.
When a class is augmented to allow creation of multiple instances of the data, we use the name object to denote a single instance of the data, along with its associated methods. In J, an object has a locale of its own, in which the data resides; the locale of the class is in the path of the object's locale, which makes the methods in the class available through name searches starting in the object's locale.
A class that supports objects must define methods named create and destroy that initialize the object's locale when the object is created and destroy the object's locale when the object is no longer needed.
Templates for defining a class that supports objects are given in Figures 3 and 4.
Locale specifier |
coclass 'objclass' |
Initial Values |
oval =: 0 |
create Method |
create =: verb define initialization that depends on y ) |
destroy Method |
destroy =: verb define release acquired resources here codestroy '' ) |
Other Methods |
method =: verb define verb definition here ) |
Public interface |
not used for objects cmethod_z_ =: cmethod_objclass_ |
Figure 3. Template for a base class that supports objects |
Locale specifier |
coclass 'derobjclass' |
Base class |
coinsert 'baseobjclass' |
Initial Values |
oval =: 0 |
create Method |
create =: verb define create_baseobjclass_ f. y other initialization here 0 ) |
destroy Method |
destroy =: verb define release acquired resources here destroy_baseobjclass_ f. y ) |
Other Methods |
method =: verb define verb definition here ) |
Public interface |
not used for objects cmethod_z_ =: cmethod_derobjclass_ |
Figure 4. Template for a derived class that supports objects |
Creating an Object
A new object is created by execution of
objname =: conew 'classname' create__objname objparms
or
objname =: objparms conew 'classname'
where classname is the name of the locale the class is loaded in. The second form is normally used, but you can use the first form if the create method requires a left argument or returns a result you need to keep.
Either form of object-creation code first creates a numbered locale which will hold the object's instance of the data; then puts classname into the path of the object locale; then calls the create verb in the object's locale to initialize the object. Since the object's locale has just been created and doesn't have any verbs in it, this call to create will execute the create verb defined in the locale classname, but it will run it in the object's locale so that the initialization is performed in the object's locale. If the class is a derived class, its create verb should call the create verb in the base class to perform any initialization needed by that class.
The result of conew is the name of the object locale. This value should be saved in the variable that will be used to refer to the object.
For example, to create a Grid Control object, use
grid=: '' conew 'jzgrid'
and thereafter use grid as the object name, for example calling show__grid to display the grid.
Note that default initial values for each object may simply be defined in the class. When an object refers to them, they will be found in the class if they have not been modified in the object. If a method in the object changes one of these values, the updated value will be written into the object's locale, where it will thereafter override the value in the class.
Calling an Object's Methods and Reading Its Contents
The methods in an object are called using locatives. The name returned by conew is used as the explicit locale of the locative. To call the method verb, use
[operand] method__objname operand
Nouns in the object can be referred to by locative, as in
publicnoun__objname
Note that the methods referred to through locatives do not need to be published as global names in the z locale.
Note that object may contain other objects. In a locative of the form simplename__var, var may be a locative, as in method__subobjname__objname, where method is sought in the locale given by subobjname__objname .
Destroying an Object
When an object is no longer needed, call destroy__objname to remove it. A class's destroy verb should release any resources that were allocated when the object was created, and if the class is derived, it should call the destroy method in the base class so that it can do likewise. The last act in destroying an object is to issue codestroy, which erases the object's locale.
Calling Class Methods in a Class That Supports Objects
It is perfectly acceptable for a class that supports objects also to publish methods that deal with the class as a whole. Such methods must use PublishEntryPoints or define names in the z locale.
Watch That f. When Calling create From a Derived Class
If you were paying close attention you noticed that the create method in a derived class calls the create method in the base class with a line like
create_baseclass_ f. y
What is the purpose of the f.? Why not just create_baseclass_ y?
The trick is that (create_baseclass_ f.) looks up the name create_baseclass_ and returns its value after substituting defined names with their values. The result of (create_baseclass_ f.) is a verb but not a locative. It executes the same code as create_baseclass_, but it doesn't switch locales before it does so, so the operation is executed in the object's locale.
Utility Verbs to Use With Objects
costate gives a list of objects and associated information.
conl y lists the named (if y contains 0) or numbered (if y contains 1) locales.
conouns"0 localelist gives a list of nouns that can be used to refer to locales in localelist .
[prefix] copathnlx types displays names of the specified types in every locale in the path at which the verb is executed. copathnlx__obj can be used to display names defined in an object. The types are any combination of 0-3 for noun, adverb, conjunction, or verb, or 6 for locale; omitted types lists names of all types. If prefix is specified, only names starting with a letter in prefix are listed.
Good Object-Oriented Design in J
Some C programmers get object-happy and make every little array its own object. Don't do this in J. If you keep your data in arrays or boxed arrays, you have the full power and high efficiency of J's array-processing verbs. Use objects when you have a substantial amount of processing that you want to be able to reuse. Good examples of objects are the Grid Control, in which the object is the entire grid of data to be displayed and edited, and the Plot Control, in which the object is the graph or surface to be plotted.
Other Uses of Locales in J
By following the guidelines given above you will be able to emulate the class facilities of C++. Because J is interpreted, you can do much more if you want: you can install verbs in objects in addition to the usual nouns; you can change search paths dynamically; you can use locales and paths to create a high-performance network database; you can pass locales as data and use them to direct processing; you can peek at a module's private data. You can even modify a module's private data from outside the module, but if you are struck by lightning after doing so the coroner will find it was suicide.
Using locale-names as data allows for dynamic separation of namespaces. For example, the processing of forms in J requires definition of verbs for a great many events. You may let these verbs all share the same locale; but if you want to segregate them, the Window Driver will remember what locale was running when each form was displayed, and direct events for a form to the locale handling the form.
>> << Pri JfC LJ Phr Dic Voc !: Rel NuVoc wd Help J for C Programmers