REF OBJECTCLASS Jonathan Meyer & Steve Knight, 1992/93 Revised by Robert Duncan, Nov 1995 AS: fixed wrapper_invoker entry 29 Mar 2006 COPYRIGHT University of Sussex 1996. All Rights Reserved. >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> <<<<<<<<<<<<<<<<<<< >>>>>>>>>>>>>>>>>>> <<<<<<<<<<<<<<<<<<< OBJECT-ORIENTED MULTI-METHOD >>>>>>>>>>>>>>>>>>> <<<<<<<<<<<<<<<<<<< EXTENSION TO POP-11 >>>>>>>>>>>>>>>>>>> <<<<<<<<<<<<<<<<<<< >>>>>>>>>>>>>>>>>>> <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< The objectclass library defines an object-oriented, multi-method extension to Pop-11 based on keys (see REF * KEYS). For an overview of the library see HELP * OBJECTCLASS. CONTENTS - (Use g to access required sections) 1 Classes 1.1 The Different Kinds of Class ... Standard Classes ... Mixin Classes ... Singleton Classes ... Extant Classes ... Note on Recompiling Classes ... Class Define-Forms 1.2 Inheritance ... The Class Precedence List ... Inheritance Syntax 1.3 Slots ... Shared Slots ... The Order of Slots ... Slot Syntax 1.4 Class Wrappers ... Wrapper Syntax ... Alternative Wrapper Syntax 1.5 Operations on Classes 2 Objects 2.1 Creating Objects ... The class cons procedure ... The class new procedure ... The create_instance procedure ... The instance syntax forms 2.2 Operations on Objects ... Printing Objects 3 Generic Procedures and Methods 3.1 Overview ... Methods ... Generic Procedures ... Method Dispatch ... Implementation Notes 3.2 Syntax of Method Definitions 3.3 Method Wrappers 3.4 Method Chaining 3.5 Operations on Generic Procedures ... Tracing Methods ... Cancelling Methods 3.6 Pre-Defined Generic Procedures 4 Browsing Classes and Generic Procedures 4.1 Name Filters 5 Objectclass and Popc 6 Objectclass Procedures and Variables ---------- 1 Classes ---------- 1.1 The Different Kinds of Class --------------------------------- The objectclass library distinguishes four kinds of class: # standard classes, created by define_class # mixin classes, created by define_mixin # singleton classes, created by define_singleton # extant classes, created by define_extant Currently, there is no way of creating a class other than by using one of these define-forms. ... Standard Classes --------------------- A standard class is a record-type key (see REF * KEYS) created by conskey, but with extra properties maintained by objectclass. When a new class (key) X is created by define_class, it is assigned to the identifier X_key. Associated with the class will be a standard set of procedures assigned to the following identifiers: consX the class constructor (based on the class_cons of X_key) destX the class destructor (same as the class_dest of X_key) newX the class new procedure isX the class recogniser define_class will also create generic procedures and methods as required for access to any specified slots. The following example illustrates the use of the define-form and the identifiers it creates: define :class Item; slot itemPartNo == false, itemDescription == 'Unknown Item'; enddefine; Item_key => ** consItem('XXX-99-1', 'Large Widget') => ** vars item = newItem(); item => ** itemDescription:Unknown Item> item.isItem => item.itemPartNo => ** 'XXX-99-1' -> item.itemPartNo; 'Large Widget' -> item.itemDescription; item => ** item.destItem => ** XXX-99-1 Large Widget The behaviour of define_class is similar to that of the standard defclass construct (see REF * DEFCLASS). The principal differences are: # the cons procedure may have extra code added to the basic class_cons procedure if the class declares any cons wrappers # the new procedure has no equivalent in defclass # the is procedure recognises not only direct instances of the class itself, but also instances of any derived classes subsequently declared # the slot access procedures are generic and so may be overloaded on different classes Moreover, the name of the newly-created class (Item in the preceding example) may be used in an inheritance specification in a later class definition, or as an argument class constraint in a method definition. ... Mixin Classes ------------------ A mixin class (or mixin for short) has similar properties to a standard class except that it cannot be instantiated, so there are no cons, new or dest procedures created for it. Also, to ensure that instances cannot be created by back-door means, the mixin assigned to the _key variable is not a true key at all, but a dummy item used by objectclass to identify the mixin, and so does not support the usual class_ operations defined on keys (such as class_cons). An intended use of mixins is to define supplementary state and behaviour which can be "mixed-in" to enrich later class definitions, but they can also be used to create abstract classes which will have concrete implementations -- as standard or extant classes -- derived later. There is also an efficiency benefit to using mixins wherever instances are not required. ... Singleton Classes ---------------------- Singleton classes are proper classes, but are intended to have only one, unique instance. So again, cons, dest and new procedures are not defined for them; instead, the define_singleton form creates the single instance itself and assigns it to the class name. For example: define :singleton NoItem is Item; slot itemDescription = 'Bogus Item'; enddefine; NoItem => itemDescription:Bogus Item> In order to have even one instance, a new key has to be created for the singleton class and is made available through the _key variable as for a standard class. This leaves open the possibility of creating additional instances of the class by using the class_cons procedure directly, but this is clearly against the spirit of the definition. ... Extant Classes ------------------- The define_extant form is designed to help integrate objectclass into the existing Poplog class system. Defining an extant class X does not create a new key, but takes an existing key from the identifier X_key and promotes it as close as possible to full objectclass status, as if it had been declared by define_class. For this to work at all, the number and types of slots declared for the class must match exactly the *class_field_spec of the existing key. But even so, there are limits to what can be achieved: typically, a pre-existing key will already have cons, etc. procedures defined for it whose definitions may conflict with what is required by objectclass and which may -- especially for system keys -- be declared constant and/or protected and so cannot easily be redefined. The steps taken by objectclass, then, to promote an extant class are as follows: # cons and dest procedures are not defined at all: existing versions, if available, can continue to be used, but will ignore any cons wrappers declared for the class # the standard new procedure is created # the is procedure is created in the usual way so that it will recognise any subclasses defined later; any existing recogniser of the same name previously declared constant or protected will be cancelled first, but this does mean that the new version will not be visible to code already compiled # slot access methods are created as normal, but any existing field-access procedures with the same names are not explicitly cancelled first, so any which cannot be redefined will cause a mishap; such problem slots can be declared with different names to the existing field-access procedures, but of course those original access procedures will remain and could still be used for object access and update, by-passing objectclass mechanisms The following example incorporates the existing pair and nil classes, used for constructing lists, into an abstract Sequence class: define :mixin Sequence; enddefine; define :mixin List is Sequence; enddefine; define :extant nil is List; enddefine; define :extant pair is List; slot first, rest == []; enddefine; newpair() => ** [undef] [1 2 3 4].isSequence => ** [1 2 3 4].first => ** 1 ... Note on Recompiling Classes -------------------------------- A potential problem can arise when recompiling a define_class statement (or one of its relatives) for which records of the class have already been created and are still in existence. Since all objects are identified by their keys (which are unique to each class) the creation of a new key on recompilation would invalidate all such existing structures using the old key, in the sense that they would no longer be recognised by the procedures associated with the new key. To obviate this problem, objectclass operates as follows: If a key identifier X_key already exists for the class name X being defined, and contains a key whose specifications -- in terms of slots, superclasses and wrappers -- exactly match the new definition, then the existing class is reused instead of a new one being created. Thus any existing instances continue to belong to the recompiled class. If you really want to create a new class, you can stop the old key being reused by assigning any non-key value to X_key before the define_class statement, e.g: undef -> X_key; Alternatively, you can assign to pop_oc_reuse (see below). ... Class Define-Forms ----------------------- define_class [define_form syntax] Defines a new standard class: define :class [declaration] name [attributes] [;] [fieldspecs] enddefine Only the class name is mandatory and the definition will declare and assign the following identifiers derived from name, as described above: name_key consname destname newname isname together with any slot access and update procedures required by slot specifications in fieldspecs. The optional parts of the definition are as follows: declaration Specifies the default declaration for identifiers created by the definition. May be any of the words vars constant lvars lconstant If omitted, the default is derived from the prevailing compile_mode (see HELP * COMPILE_MODE) as with any other define-form. attributes A (square) bracketed list of attribute names (words) to be passed in the attribute_vec argument to conskey (see REF * CONSKEY for legal values and their meanings). Note that if neither "writeable" nor "nonwriteable" is specified explicitly, a default will be supplied according to the value of pop_oc_writeable_default described below. fieldspecs A list of fields describing the inheritance, slots and wrappers of the new class using the is, slot and on syntax forms described below. define_mixin [define_form syntax] Defines a new mixin class: define :mixin [declaration] name [attributes] [;] [fieldspecs] enddefine where the declaration, name, attributes and fieldspecs are as for define_class above. Creates the following identifiers: name_key isname together with any slot access and update procedures required by slot specifications in fieldspecs. define_singleton [define_form syntax] Defines a new singleton class: define :singleton [declaration] name [attributes] [;] [fieldspecs] enddefine where the declaration, name, attributes and fieldspecs are as for define_class above. Creates the following identifiers: name name_key isname together with any slot access and update procedures required by slot specifications in fieldspecs. define_extant [define_form syntax] Declares a new extant class: define :extant [declaration] name [;] [fieldspecs] enddefine where the declaration, name and fieldspecs are as for define_class above; no attributes can be sensibly specified, because the definition does not create a new key. The definition creates the following identifiers: newname isname together with any slot access and update procedures required by slot specifications in fieldspecs. 1.2 Inheritance ---------------- Inheritance is specified in a class definition by one or more clauses of the form: is ; The keyword isa can be used interchangeably in place of is according to taste. The is a comma-separated list of names, each of which must be already defined as a class, i.e. there must be a _key identifier in scope for it. More than one class name is allowed, to support multiple inheritance. Unlike some other class-based object systems, objectclass has no notion of an implicit "root" class from which all other classes are ultimately descended: if a class is defined without any inheritance explicitly specified, then it inherits nothing. (The fact that generic procedures support default methods compensates for what might otherwise appear to be an omission. See below.) If a class B inherits from class A, then B is said to be derived from A; alternatively, B is a subclass of A and A a superclass of B. The subclass/superclass relation is transitive: if class C is subsequently declared as a subclass of B, then C is also implicitly a subclass of A; B is called the direct superclass of C, and A an indirect superclass. Sometimes it is convenient to think of the relation as transitive too, so that every class is a subclass (and superclass) of itself. But no class can really inherit from itself, either directly or indirectly. As an example, the definition define :class CompoundItem is Item; slot itemParts == []; enddefine; creates a new class CompoundItem derived from Item. An immediate consequence of the derivation is that every instance of CompoundItem is also considered an instance of Item, and the isItem recogniser will recognise CompoundItems too: newCompoundItem().isItem => ** The inverse is not true, however: newItem().isCompoundItem => ** So inheritance is strictly one-way, as might be expected. A derived class inherits specific properties from its superclasses, namely: # slots # wrappers # methods Every instance of CompoundItem has the slots of an Item, in addition to the itemParts slot declared explicitly within the class definition: vars item = newCompoundItem(); 'YYY-58-4' -> item.itemPartNo; 'Complex Gadget' -> item.itemDescription; item => ** Likewise, any methods defined on Items will also work on CompoundItems: define :method report(x:Item); printf('Item:\t'); pr_field(x.itemPartNo, 12, false, `\s`); npr(x.itemDescription); enddefine; report(item); Item: YYY-58-4 Complex Gadget ... The Class Precedence List ------------------------------ Through multiple inheritance and the transitivity of inheritance, a class will typically have many superclasses. If a class C has superclasses S1,...,Sn and just one of those superclasses defines a method M, then that method is inherited unambiguously by C. But in the case where more than one superclass provides an explicit definition for M, which definition should be inherited by C? Any potential ambiguities are resolved in objectclass by construction of a class precedence list (CPL) for each class. The CPL is a linearization of the set of superclasses, imposing a total ordering on superclasses S1 < S2 < ... < Sn If two or more superclasses Si,Sj,... define a method M, then class C inherits M from whichever of the Si,Sj,... comes first in the CPL. The CPL for a class is constructed by merging the CPLs of its direct superclasses, in such a way as to preserve their respective class orderings. Preserving order is important for consistency, as it ensures that a class cannot inherit anything that isn't defined or inherited by one of its direct superclasses (i.e. inheritance can't skip a generation). There may be more than one arrangement of superclasses which satisfies this requirement, but it's rarely necessary to know exactly which one is chosen by objectclass except in obscure cases. Most of the consequences of the CPL order can be inferred from the following rules: (1) A class C always precedes its proper superclasses S1,...,Sn in any CPL in which it occurs. So if C defines its own variant of method M, then C and any subclass of C will always prefer that variant of M over any defined by S1,...,Sn. This allows a class to override methods defined by its superclasses. For example: define :class X; enddefine; ;;; define :method whoami(x:X); 'An instance of X'; enddefine; ;;; newX().whoami => ** An instance of X define :class Y is X; enddefine; ;;; define :method whoami(x:Y); 'An instance of Y'; enddefine; ;;; newY().whoami => ** An instance of Y define :class Z is Y; enddefine; ;;; newZ().whoami => ** An instance of Y The definition of method whoami for Y overrides the definition on X, both for Y and any subclass of Y. (2) Direct superclasses are always placed in declaration order. If the definition of a class C contains the inheritance specification is D1,...,Dk ; then the ordering D1 < ... < Dk will be maintained in the CPLs of C and any subclasses of C. So the designer of a class can choose which superclasses should be preferred for inheritance. For example: define :class A; enddefine; ;;; define :method whoami(x:A); 'An instance of A'; enddefine; define :class B; enddefine; ;;; define :method whoami(x:B); 'An instance of B'; enddefine; define :class C is A,B; enddefine; ;;; newC().whoami => ** An instance of A define :class D is B,A; enddefine; ;;; newD().whoami => ** An instance of B Here, by changing the order of the direct superclasses A and B, the class designer can choose which version of the whoami method is inherited by the derived classes C and D. The rationale behind these rules and details of the algorithm for constructing the CPL are discussed in R.Ducournau, M.Habib, M.Huchard, M.L.Mugnier Proposal for a Monotonic Multiple Inheritance Linearization OOPSLA '94, ACM SIGPLAN Notices, 10/94, pp. 164--175 An example Pop-11 algorithm is presented in TEACH * INHERITANCE. It is very easy to write down a class definition for which it is impossible to construct a class precedence list satisfying these constraints. For example, building on the example at (1) above: define :class ? is X,Y; enddefine; Rule (2) demands that X precede Y, but rule (1) demands the opposite, and objectclass will refuse to compile this definition. Fortunately, such problem cases rarely cause difficulties in realistic programs. ... Inheritance Syntax ----------------------- is class-list ; [objectclass syntax] isa class-list ; Declares inheritance of a class. The class-list is a comma-separated list of names which must all have been previously defined as classes using one of the objectclass define-forms. Those classes become superclasses of the new class being defined. adopting class-list ; [objectclass syntax] Declares the new class being defined as a superclass of the existing classes identified by class-list. The existing classes must be compatible with the new class in the sense that their slots must be a superset of the new class's slots. 1.3 Slots ---------- The slots of a class determine the private state of its instances. An instance carries a value for each slot which can be accessed and updated independently of the value held by any other instance. Slots are like the instance variables of other object-oriented languages, but are read and modified by generic procedure calls, requiring no special syntax. A slot is characterised by three attributes: a name, which determines the generic procedure used to access and update the slot; a typespec, which can restrict or modify values assigned to the slot, and an initial value, which is assigned to the slot for each instance created by the class new procedure. Slots are inherited, so the full set of slots possessed by a class is the union of those slots explicitly declared in the class definition together with the slots of its direct superclasses. More than one superclass may declare a slot with the same name, but slots are unique, so the derived class will have only one slot with that name. The type and initial value for the slot are taken from the first class in the class precedence list which defines the slot (NB: duplicate slots should really have compatible types, but this is not checked). Slots are specified as part of a class definition in clauses of the form: slot ; A slot with all its attributes specified might look like this: slot timeStamp :int = theCurrentTime(); Here, the slot name is timeStamp: the class definition will create a generic procedure with this name (if it doesn't exist already) and add to it methods which access and update the slot. The typespec is :int, which stores the value as a machine integer, presumably so that instances of this class can be passed to externally-loaded procedures (objectclass supports the same set of typespecs allowed by defclass; see REF * DEFSTRUCT). The initial value is computed for each new instance by calling the procedure theCurrentTime. Only the slot name must be specified; the slot type defaults to "full" which allows any Poplog value to be assigned, and the initial value defaults to "undef" for full fields and 0 for numerics. This is the same as defclass. The definition of extant class pair above illustrates typical partial slot specifications: slot first, rest == []; This declares two slots, first and rest, both full fields, with rest initialised to []. Hence: newpair().first => ** undef newpair().rest => ** [] These examples also show the two different ways in which an initial value can be specified, using either single (=) or double (==) equals. The distinction is this: an "==" initialiser is evaluated exactly once, at the time the class definition is compiled, and the resulting value saved away to be assigned to the slot for each new instance created; an "=" initialiser is evaluated afresh for every instance, and so can return a different value each time (such as a timestamp). Code to evaluate any "=" initialisers is compiled into the class new procedure. While this is executing, the variable myself is bound to the instance being created, so initialisers can use this to change their behaviour depending on the values already assigned to other slots (although complex initialisations like this are better handled with a class new wrapper, described below). ... Shared Slots ----------------- An alternative form of slot specification: shared_slot ; can be used to declare shared slots (also known as class slots; ordinary, non-shared slots are sometimes called instance slots when the distinction is important). A shared slot associates a value with the class itself, rather than with each instance: accessing a shared slot will return the same value for all instances of the class, and updating it will change that value for all instances. While a shared slot description allows both forms of initialiser, the difference between "=" and "==" initialisation is rather more subtle than before. Since all instances are sharing a single slot, the initial value for that slot is only ever computed once. The different forms of initialiser control when that computation is done: the "==" form computes its value when the class is defined, as before, but the "=" form computes its value on the first occasion when the slot is accessed. If the slot is never accessed, or is explicitly assigned before being accessed, the initial value is never computed. This may be important if the computation is lengthy. Shared slots can be defined separately from their class definitions: see define_shared_slot below. ... The Order of Slots ----------------------- An objectclass instance is a Poplog record, with the class as its key and one field for each instance slot. There are three circumstances in which the order of slots within the record may be of significance: # when using the class cons procedure, which expects one argument for each slot in record order # when accessing slots using non-objectclass procedures, e.g. class_access, sysFIELD, etc. # when passing an instance to an externally-loaded procedure The arrangement of slots can be determined by the following method: # start with the class definition, removing everything except slot and is (inheritance) specifications; # recursively replace each direct superclass with its own list of slots; # remove duplicates, with earlier slots taking precedence over later occurrences. For example, given define :class A; slot s1, s2; enddefine; define :class B; slot s2, s3; enddefine; define :class C is A,B; slot s1, s4; enddefine; the order of slots for class C can be determined as follows: ;;; isolate slots and superclasses is A, B; slot s1, s4; ;;; recursively expand superclasses slot s1, s2; slot s2, s3; slot s1, s4; ;;; remove duplicates slot s1, s2, s3, s4; Note that this ordering is independent of the types and initial values associated with each slot, which are determined through the usual class precedence rules. The default pointer position for a class is determined in the same way as for any recordclass created by conskey, but you can set the pointer position explicitly by placing the pointer symbol >-> in front of a slot, as in define :class Point; >-> slot xCoord :short, yCoord :short; enddefine; which forces slot xCoord to the pointer position (see REF * DATA). ... Slot Syntax ---------------- slot slot-list ; [objectclass syntax] Declares one or more instance slots. The slot-list is a comma-separated sequence of slot specifications, each of the form [declaration] name [typespec] [initialiser] Only the name is mandatory and names the generic procedure used to access the slot. The optional components are as follows: declaration Declaration for the generic procedure name. May be any of the words vars constant lvars lconstant If omitted it defaults to the declaration specified for the class as a whole. typespec Declares the type of the slot. Valid forms are described in REF * DEFSTRUCT. The default is "full". initialiser Declares an initial value for the slot. There are two alternative forms = expression == expression The difference is explained above. The default is "undef" for full slots and 0 for numerics. shared_slot slot-list ; [objectclass syntax] Declares one or more shared (or class) slots. The slot-list is as for a slot declaration described above. 1.4 Class Wrappers ------------------- Wrappers add extra code to the procedures created by a class definition, to be executed at significant points during the lifetime of each instance. The supported wrapper types are: cons run after an object has been created with the cons procedure new run after an object has been created and initialised with the new procedure access run each time an instance slot is accessed update run each time an instance slot is updated destroy run when an object is reclaimed by the garbage collector Wrappers of all kinds accumulate through inheritance and are executed in inheritance order, either forwards or backwards through the class precedence list depending on the wrapper type. Currently, you cannot override or cancel superclass wrappers. A wrapper is specified in a class definition by a clause of the form: on () do ; The is evaluated for each instance at the point indicated by the which must be one of those listed above. The declares a set of identifiers which are bound during evaluation of the wrapper; this will include at least a name for the current instance. An example of using wrappers: define :class CountedItem is Item; ;;; keep a count of the number of items in circulation shared_slot counter == 0; on cons(item) do item.counter + 1 -> item.counter; on destroy(item) do item.counter - 1 -> item.counter; enddefine; newCountedItem().counter => ** 1 newCountedItem().counter => ** 2 newCountedItem().counter => ** 3 sysgarbage(); newCountedItem().counter => ** 1 define :class DatedItem is CountedItem; ;;; mark each item with the date slot dateStamp; on new(item) do npr(sysdaytime() ->> item.dateStamp); ;;; note any changes on update(item, p) do printf('Updated item %p\n', [^item]); enddefine; newDatedItem().counter => Updated item itemDescription:Unknown Item dateStamp:Thu Nov 16 10:16:48 GMT 1995> Thu Nov 16 10:16:48 GMT 1995 ** 2 Note here how DatedItem inherits the counting cons wrapper from the CountedItem class and also how the update wrapper is invoked when the dateStamp slot is initialised by new. ... Wrapper Syntax ------------------- on cons(variable) do expression; [objectclass syntax] The expression is evaluated each time an instance is created using the class cons procedure, with variable bound to the newly-created object. cons wrappers are run in reverse CPL order, i.e. the expression will be evaluated after any cons wrappers inherited from superclasses. Since the cons procedure is called by the new procedure, cons wrappers are also executed by new and so you should use a cons wrapper to attach code which must be executed for every instance. on new(variable) do expression; [objectclass syntax] Like a cons wrapper, but evaluated after an instance has been created and initialised with the class new procedure, i.e. after all instance slots have been assigned their initial values. Use this for more complex initialisations, or those involving multiple slots. on access(variable_1, variable_2) do expression; [objectclass syntax] The expression is evaluated before an instance slot is accessed with variable_1 bound to the object to be accessed and variable_2 bound to the access procedure that will do it, including any superclass wrappers. access wrappers are run in CPL order, i.e. the expression will be evaluated before any access wrappers inherited from superclasses. on update(variable_1, variable_2) do expression; [objectclass syntax] The expression is evaluated after an instance slot has been updated, with variable_1 bound to the object that was updated and variable_2 bound to the update procedure that did it, including any superclass wrappers. update wrappers are run in reverse CPL order, i.e. the expression will be evaluated after any update wrappers inherited from superclasses. on destroy(variable) do expression; [objectclass syntax] The expression is evaluated as a destroy action (see REF * PROPS) when the garbage collector determines there are no remaining references to an instance, with variable bound to the object being destroyed. destroy wrappers are run in CPL order, i.e the expression will be evaluated before any destroy wrappers inherited from superclasses. The destroy action is attached to each object as it is created with the class cons procedure; objectclass maintains its own, private destroy property so you cannot (either deliberately or accidentally) cancel or change a destroy action once installed. NB: destroy wrappers are only run after a garbage collection and not when an object is ``destroyed'' due to process termination. ... Alternative Wrapper Syntax ------------------------------- An alternative syntax for wrappers allows the form: on do ; Here the possible are as before; the absence of a parenthesised distinguishes the two forms. In this case the wrapper expression must evaluate to a procedure which is called at the wrapper point; the procedure should expect a single argument: another procedure which must be explicitly called to do the real work. So in the example: on cons do my_wrapper ; the procedure my_wrapper -- which should already be defined -- will be applied to a constructor procedure consisting of the class_cons procedure plus any inherited superclass wrappers. This constructor must be called in order to create an instance: arguments to the constructor will be on the stack. The form on cons(x) do ; is simply shorthand for on cons do apply <> procedure(x); ; endprocedure; Similar equivalences for other wrapper forms can be deduced. The purpose of this alternative syntax is that the wrapper procedure can choose when (or, indeed, if) to apply the underlying procedure. A cons wrapper might, for example, choose to return an object from a free list rather than allocate new store. 1.5 Operations on Classes -------------------------- isclass(class) -> word [procedure] isclass(item) -> [procedure] A recogniser for classes created by the objectclass library. Applied to such a class, isclass returns a word describing what kind of class it is; applied to any other item, it returns . Words returned by isclass may be: "mixin" if class is a mixin defined by define_mixin "singleton" if class is a singleton defined by define_singleton "object" if class is a standard class or extant class defined by define_class or define_extant Note that, although classes are typically represented by standard Poplog keys (mixin classes may be an exception), isclass will only recognise keys created by objectclass itself, or those which have been promoted to class status by define_extant. So: isclass(integer_key) => ** define :class X; enddefine; isclass(X_key) => ** object class_name(class) -> word [procedure] Returns the name of a class. The name is always a word. class_isa(class) -> isa_p [procedure] Returns the recogniser procedure for class which respects the class hierarchy, i.e, a procedure which returns if it is applied to an instance of class or one of its descendents, and otherwise. class_construct(class) -> cons_p [procedure] Returns the constructor procedure for class. This is the procedure bound to the cons procedure for the class. It consists of the class_cons procedure created by conskey, possibly augmented by wrappers defined for the class or any of its superclasses. NB: this procedure is invalid for mixins, which don't have constructors. class_new(class) -> new_p [procedure] Returns a procedure new_p which creates a new instance of class with slots set to their default values. NB: this procedure is invalid for mixins, which cannot have instances. class_slots(class) -> list [procedure] Returns a list of the slot access procedures for class. Normally these will be generic procedures. Note for experts: This procedure actually returns a list with an unshared spine. The elements of the list are the slot identities, i.e. the items used to distinguish different slots. destclass(class) -> (slot_p_1, slot_p_2, ..., slot_p_n) [procedure] Explodes all of the slot access procedure of class onto the stack. Same as class_slots(class).dl appclass(class, p) [procedure] Applies procedure p to every slot access procedure of class. Same as applist(class_slots(class), p) class_example(class) -> object [procedure] object -> class_example(class) Returns an example instance of a class. The example is created on the first call using class_new and the same object is then returned for all subsequent calls. The updater sets the class_example to a particular instance. NB: this procedure is invalid for mixins, which cannot have instances. If class is a singleton class then the class_example is the only instance of that class. class_slot(generic, class) [procedure] -> class_slot(generic, class) Applies a generic procedure (or, in update mode, the updater of a generic procedure) to the class_example of a class. Defined simply as: generic(class_example(class)) class_infs(class) -> list [procedure] Returns a list of all the direct inferiors (or children) of class. class may be a mixin, and list may contain mixins. (Note for experts: The spine of the list is guaranteed to be unshared.) class_supers(class) -> list [procedure] Returns a list of the direct superclasses (or parents) of class in inheritance order. class may be a mixin, and list may contain mixins. (Note for experts: The spine of the list is guaranteed to be unshared.) KEY_SLOT class generic [macro] This convenience macro arranges for the named procedure generic to act as a non-inheriting class-variable for class. In other words, sub-classes act as if the class variables are copied rather than shared. Uses a property to map a class to its slot value. See LIB * KEY_SLOT for more details. fast_add_superclass(parent_class, child_class) [procedure] Makes parent_class a direct superclass of child_class. This unusual ability is not intended to be used at run-time to do bizarre things to the class hierarchy. Its purpose is to allow you to incorporate existing, useful classes into a new class hierarchy, perhaps because you would suffer a performance hit or because you want to avoid some syntactic inconvenience. There is a strong restriction on the use of fast_add_superclass: you cannot add a (candidate) superclass to a (candidate) subclass if the superclass would add a new field to the subclass. This restriction prevents some of the worst potential abuses. ---------- 2 Objects ---------- An object is an instance of a standard class, singleton class or extant class. An object is a Poplog record with the class as its key and one field for each instance slot. 2.1 Creating Objects --------------------- ... The class cons procedure ----------------------------- For a class named X, the class cons procedure is assigned by objectclass to the identifier consX. The declaration for the identifier is determined by the part of the class definition. Also, for a given class you can obtain the class cons procedure dynamically by calling class_construct. The cons procedure expects one argument for each instance slot of the class and the order of arguments must match the order of fields in the instance record. The set of slots includes those inherited from superclasses. For example: consItem('XXX-99-1', 'Large Widget') => ** consCompoundItem('YYY-58-4', 'Complex Gadget', []) => ** The cons procedure creates an instance using the class_cons procedure created by conskey and then applies any cons wrappers associated with the class. If there are no wrappers, the cons and class_cons procedures are equivalent. Inheritance makes it hard to keep track of the proper number and order of slots possessed by any one class. Indeed if a superclass is a library class, its layout of slots may not even be made public. This means that it is not always easy to determine statically the arguments expected by a cons procedure. Also, if a superclass is recompiled with a different set of slot specifications, any calls to constructors of derived classes will be invalidated because their argument lists will change. For these reasons, the cons procedure should be used sparingly and should rarely be exported as part of a public interface. ... The class new procedure ---------------------------- For a class named X, the class new procedure is assigned by objectclass to the identifier newX. The declaration for the identifier is determined by the part of the class definition. Also, for a given class you can obtain the class new procedure dynamically by calling class_new. The new procedure takes no arguments and returns a new instance of its class, with slots assigned their initial values as specified in the class definition. For example: newItem() => ** itemDescription:Unknown Item> newCompoundItem() => ** itemDescription:Unknown Item itemParts:[]> The new procedure uses the cons procedure to create an instance with its slots set to default values, computes and assigns any additional initialisers specified in the class definition and then applies any new wrappers associated with the class. Because new calls cons, cons wrappers are also run, but before the initialisation step. ... The create_instance procedure ---------------------------------- create_instance(prototype, slot_inits) -> object [procedure] create_instance(prototype, slot_inits, modify_p) -> object Creates a new instance from a prototype, sets its slots according to slot_inits and then runs the optional modify_p procedure to perform additional initialisations. The prototype may be a class or an object. If it is a class, the new instance is created by calling the new procedure of that class; if it is an object, the new instance is created by calling copy on the object. NB: in this latter case, copy creates a new instance without running any cons or new wrappers defined for the object's class. slot_inits denotes an even-length sequence of alternating procedure and data items. These may be supplied in a list, a vector, or exploded on the stack with a count, i.e. [p1, item1, ..., pN, itemN] ;;; list {p1, item1, ..., pN, itemN} ;;; vector p1, item1, ..., pN, itemN, 2*N ;;; stack Procedures will typically be slot access procedures, but need not be. create_instance calls the updater of each procedure in turn to assign the corresponding data item to the newly-created object. If supplied, modify_p must be a procedure that takes one argument and returns no results. It is applied to the new object after all the slot_inits have been run. Example: create_instance(CompoundItem_key, [ ^itemDescription 'Complex Gadget' ^itemPartNo 'YYY-58-4' ]) => ** ... The instance syntax forms ------------------------------ instance [syntax] Creates an anonymous object with initialisation. Has the form: instance class-name initialisers endinstance where class-name denotes an existing class. This creates and returns a new object by calling the new procedure of the named class. The object is then further initialised through the sequence of initialisers each of the form: procedure-name = expression ; The procedure-name must denote a generic procedure with an updater applicable to an object of this class; typically it will be a slot name, but need not be. The expression is evaluated and the result assigned to the named updater applied to the newly-created object. Example: instance CompoundItem itemDescription = 'Complex Gadget'; itemPartNo = 'YYY-58-4'; endinstance => ** define_instance [define_form syntax] This is a variation on the above which gives a name to the object created. It takes the form define :instance declaration name : class-name ; initialisers enddefine The class-name and initialisers are as before and the process of creating and initialising an object is the same as for the instance form. The difference is that a new identifier name is defined and bound to the newly-created object; the declaration for the identifier is determined from the declaration part which has the usual options. For example: define :instance gadget :CompoundItem; itemDescription = 'Complex Gadget'; itemPartNo = 'YYY-58-4'; enddefine; gadget => ** 2.2 Operations on Objects -------------------------- isinstance(item) -> bool [procedure] Recogniser for objectclass instances. Returns if item is an instance of an objectclass or otherwise. Defined as isclass(datakey(item)) set_slots(object, slot_inits) [procedure] Applies to an object a sequence of updates described by slot_inits. The possible formats of slot_inits are as described for create_instance above. ... Printing Objects --------------------- Printing of objects is controlled by sys_print_instance. This is assigned to the class_print of each new class created by objectclass, and so is the default printer used for all objects. You can change this default in the usual way (see HELP * CLASS_PRINT) but this is not recommended: sys_print_instance uses the generic procedure print_instance to actually display an object, so you can change the way objects are printed by adding methods to that as described below. The virtue of having sys_print_instance as the default printer is that it will avoid calling print_instance in the following circumstances: # when the value of pop_pr_level falls to 0: in this case, sys_print_instance switches to "minimal" printing, where the display of each object is truncated to (the variable pop_oc_print_level is treated in the same way as pop_pr_level but applies only to objectclass printing) # if a loop is encountered in the structure being printed: in this case, sys_print_instance prints just "(LOOP)" rather than calling print_instance for a second time on the same object; this behaviour is independent of the print level and can be controlled with the flag pop_oc_print_loop_check Having these checks performed by sys_print_instance means that problems of print depth and recursion can safely be ignored by print_instance methods. sys_print_instance(item) [procedure] Default print procedure assigned to the class_print of each new key created by objectclass. pop_oc_print_level -> int [variable] pop_oc_print_level -> false int -> pop_oc_print_level false -> pop_oc_print_level Similar to *pop_pr_level but applies only to objects printed by sys_print_instance, i.e. typically only objects created as instances of objectclasses. Unlike pop_pr_level, it may be assigned (the default) in which case it is ignored. pop_oc_print_loop_check -> bool [variable] bool -> pop_oc_print_loop_check If (the default) sys_print_instance will check for cycles in the structures it prints and avoid going into an infinite loop by printing "(LOOP)" instead of calling print_instance. Setting this suppresses the checks and so can improve printing speed. --------------------------------- 3 Generic Procedures and Methods --------------------------------- 3.1 Overview ------------- ... Methods ------------ In objectclass, a method is a procedure constrained to apply only to arguments which are instances of specified classes. These class constraints are expressed as part of the method header. For example: define :method isPartOf(x:Item, y:CompoundItem); member(x, y.itemParts); enddefine; Here, the arguments x and y are constrained to be instances of the classes Item and CompoundItem respectively. It is the responsibility of objectclass to ensure that these constraints are respected, such that at the start of execution of the method body, the condition isItem(x) and isCompoundItem(y) is guaranteed to be true. In all other respects, the definition of a method is the same as that of any other procedure. The combination of method name and argument class constraints determines the signature of a method: the example above has signature isPartOf(Item, CompoundItem) A program may define any number of methods of the same name, provided that they all have different signatures. If more than one method definition specifies the same signature, the last to be compiled takes precedence and the earlier definitions are discarded. Not all the arguments to a method need be constrained: the define header :method isPartOf(x, y:CompoundItem); leaves x unconstrained, while :method isPartOf(x, y); has no constraints at all. The signatures of these methods can be written isPartOf(_, CompoundItem) isPartOf(_, _) A method which has no constraint on a particular argument is called a default method for that argument, and a method with no constraints at all is called simply the default method. An updater method is defined by adding the updaterof keyword to the definition, as for an ordinary procedure: define :method updaterof isPartOf(flag, x:Item, y:CompoundItem); if not(flag) then delete(x, y.itemParts) -> y.itemParts; elseif not(isPartOf(x, y)) then x :: y.itemParts -> y.itemParts; endif; enddefine; This method has signature: ->isPartOf(_, Item, CompoundItem) A slot specification in a class definition implicitly defines two methods: one to access the slot and one to update it. The definition define :class Item; slot itemPartNo, itemDescription; enddefine; creates four new methods: itemPartNo(Item) ->itemPartNo(_, Item) itemDescription(Item) ->itemDescription(_, Item) Likewise for a shared slot (see also define_shared_slot below). ... Generic Procedures ----------------------- A set of methods all with the same name is called a generic procedure. A generic procedure can be pictured as a box containing its method parts: -- isPartOf ------------------------ | | | isPartOf(Item, CompoundItem) | | isPartOf(_, CompoundItem) | | isPartOf(_, _) | | | ------------------------------------ As a whole, the procedure box implements some logical or abstract operation, while the individual method parts provide the implementations for that operation appropriate to their argument types. In objectclass, although you can add methods to the box with define_method and delete them with cancel_method, you can never get inside the box, i.e. you cannot access methods directly. This is to avoid the danger of a method being applied to arguments for which it was not designed. You can create an empty generic procedure with the define-form define_generic, but this is not necessary: the first time a method is defined with a particular name, a generic procedure containing just that single method is created automatically. Subsequent definitions with the same name are added to the existing procedure. A generic procedure is a true Poplog procedure and so can be used in any context where a procedure is expected. When applied, a generic procedure selects one of its methods to run based on the classes of the arguments supplied to the call. The process of choosing and applying a method is known as dispatching. In any particular application of a generic procedure there may be more than one method which could be applied to the given arguments: given the procedure isPartOf pictured above and the call isPartof(newItem(), newCompoundItem()) any of the three available methods could be applied. In such a case a single method is chosen using a deterministic algorithm described below. It can also occur that there is no method whose signature matches the classes of the arguments supplied; in this case the dispatch fails, typically causing a mishap. A generic procedure with a default method can never fail. ... Method Dispatch -------------------- Consider a generic procedure P having n methods -- P ------------------ | | | P1(C11,...,C1m) | | P2(C21,...,C2m) | | ... | | Pn(Cn1,...,Cnm) | | | ----------------------- and an application P(X1,...,Xm) The dispatch algorithm must select and apply one of the methods P1,...,Pn most appropriate to the arguments (X1,...,Xm). This is a three-step process: (a) Determine the set of applicable methods. A method Pi is applicable to arguments (X1,...Xm) precisely when for each k (1 <= k <= m) either Pi is the default method for k (so Cik = _) or Xk is an instance of Cik (so isCik(Xk) is true). If the set of applicable methods is empty, dispatch fails. If the set contains only a single method, then that must be the one to apply. Otherwise, move on to step (b). (b) Sort the set of applicable methods. Suppose there are s applicable methods, s <= n, denoted P1,...,Ps. For a particular argument Xk we can define a total order on the constraints C1k,...,Csk based on the class precedence list of the class of Xk. For any two constraints Cik and Cjk, Cik /= Cjk, define Cik < Cjk whenever Cjk = _ (so that Pj is a default method for k) or when Cik precedes Cjk in the class precedence list of Xk. This ordering on constraints extends to method signatures by applying a right-to-left lexicographical ordering: i.e, Pi < Pj if, for the largest k (1 <= k <= m) for which Cik /= Cjk, Cik < Cjk. The choice of the rightmost argument as being the most significant will come as no surprise to users familiar with Pop-11's stack semantics where the rightmost argument to a procedure call is on top of the stack. (c) Apply the least member of the sorted set. That there must be a unique least member follows from the requirement that all method signatures be distinct. If this algorithm seems complicated, it is often sufficient to remember that a method defined on a derived class takes precedence over methods defined on its superclasses. This is a requirement for the objectclass style of object-oriented programming, where a derived class may be expected to specialise the behaviour of its superclasses. An example of dispatch: define :method example(x:Item, y:CompoundItem); npr(';;; Selected: example(Item, CompoundItem)'); enddefine; define :method example(x:CompoundItem, y:Item); npr(';;; Selected: example(CompoundItem, Item)'); enddefine; example(newCompoundItem(), newCompoundItem()); ;;; Selected: example(Item, CompoundItem) Here, the condition CompoundItem < Item will always be true because CompoundItem is a subclass of Item. Sorting on the rightmost argument first gives example(Item, CompoundItem) < example(CompoundItem, Item) ... Implementation Notes ------------------------- Method dispatch is not as expensive as it may sound: determining the least applicable method can be reduced to a sequence of inline key tests and branches. However, the required tests change as new methods are added to a generic procedure, or as new classes are added to the class hierarchy. So an objectclass generic procedure has two forms: linked and unlinked. Generic procedures are created unlinked: in this form, new methods can be added to the procedure at little cost. When an unlinked procedure is applied for the first time, inline dispatch code is generated and executed on the fly; this is expensive, but the code is saved so that subsequent calls can execute it directly. The procedure is then in linked form. If subsequently a new method is added to the procedure, or some change occurs in the class hierarchy which invalidates the dispatch code, that code is discarded and the procedure returned to unlinked form, to be relinked on the next call. The overhead of calling a linked generic procedure -- as compared to a standard Pop-11 procedure doing the same job -- is typically quite small. A linked generic procedure is a closure of a specialised dispatch procedure which selects and chains a particular method. From application of a generic procedure to execution of a particular method body, the sequence of events is closure call ;;; the generic procedure procedure call ;;; the dispatch procedure tests ;;; dispatch code procedure call ;;; selected method Compare this to a standard procedure call which -- to do the same job -- would have to include its own tests to ``dispatch'' to a particular block of code: procedure call ;;; the procedure itself tests ;;; dispatch code This implies a small cost of one closure call and one procedure call for each generic. However, benchmarks suggest that the dispatch code generated by the objectclass linker can often outperform hand-written tests, further reducing the margin. Two special cases require further mention. Firstly, if a selected method corresponds to a slot access or update, fast code for this is generated inline, avoiding the need for the final method call. Thus slot access compares favourably with similar recordclass field access. Secondly, although in general the complexity of the class hierarchy will increase the average number of tests required for dispatch, mixin classes -- which have no instances -- need never be tested for. So you can improve the overall dispatch performance by using mixins wherever possible. Calling an unlinked generic procedure is always expensive. You can use optimise_objectclass to link generics explicitly. 3.2 Syntax of Method Definitions --------------------------------- The *compile_mode +oldvar flag is turned off during compilation of the following define-forms, so that input and output arguments not otherwise declared will always default to lvars. This affects any local procedure definitions, too. define_generic [define_form syntax] Declares a generic procedure without defining any methods: define :generic [declaration] name ( [input-locals] ) [output-locals] [with-list] ; enddefine Use this to satisfy a forward reference or to declare an abstract operation. The name is the name of the generic procedure to be declared. Optional parts of the declaration are as follows: declaration Declaration for name. May be any of the words vars constant lvars lconstant If omitted, the default is determined from the prevailing *compile_mode as for any other define-form except that name will always be declared as "procedure" type, regardless of the compile mode. input-locals output-locals The input and output variables of the generic, written as for an ordinary procedure definition. The number of inputs declared here determines the arity (pdnargs) of the generic procedure, unless overridden by a subsequent with_nargs clause; any methods added to this generic must have the same arity. with-list Declares additional properties of the generic procedure. The only such properties currently supported are with_props item Sets the pdprops of the generic to item; if this is omitted, the pdprops will be set to name. with_nargs N Sets the pdnargs of the generic to N; if this is omitted, the pdnargs will be set to the number of inputs. Any methods added to the generic must be defined with exactly N arguments. with_nargs variadic Declares that methods added to the generic may be defined with varying numbers of arguments. At most one explicit use of define_generic is allowed for each generic procedure: this helps guard against name clashes between files. Example: define :generic productionCost(item) -> price; enddefine; Declares a generic procedure productionCost with arity 1. define_method [define_form syntax] Adds a method to a generic procedure: define :method [updaterof] [declaration] name ( [typed-input-locals] ) [output-locals] [with-list] ; procedure-body enddefine The name identifies the generic procedure to be augmented; if this does not already exist, it is implicitly created as if by define_generic using information extracted from the method header. If the optional keyword updaterof is specified, the method is added to the *updater of the generic. The typed-input-locals declare the names, number and classes of arguments expected by the method. This is similar to the argument list of a normal procedure definition, except that each argument can be optionally followed by a class constraint, as [idprops] variable [:class-name] The class-name must denote an existing class, and constrains the method to apply only to instances of that class. For example: define :method productionCost(item:CompoundItem) -> cost; lvars part, cost = 10; for part in item.itemParts do cost + part.productionCost -> cost; endfor; enddefine; This defines a method of the generic procedure productionCost appropriate to instances of the class CompoundItem. The output-locals, with-list and procedure-body are the same as for a standard procedure definition. The procedure body is executed when the method is dispatched, with the input argument names bound to the actual arguments of the call. Any output-locals will be returned in the usual way at the end of the method call. As with ordinary procedures, methods may return results additional to those specified by their outputs, and different methods of the same generic procedure need not all declare the same outputs. Output arguments cannot have class constraints, as they do not form part of the method signature. define_shared_slot [define_form syntax] Adds a shared slot to a class: define :shared_slot [declaration] name ( variable : class-name ); procedure-body enddefine The class-name must denote an existing class. The effect is identical to declaring a shared_slot in the definition of the class, but is more convenient where the initialisation of the slot is complex: this form allows arbitrary code in the procedure-body to be evaluated when the initial value is computed, whereas a shared_slot specification allows only a single expression. The procedure-body is evaluated when the slot is first accessed with the variable bound to the particular instance being accessed; it must return exactly one result, and that same result is then returned for each subsequent slot access until the slot is explicitly updated with a new value. A shared_slot is implemented by access and update methods added to the generic procedure name, and this will be created automatically if needed. define_if_needed [define_form syntax] Implements a kind of ``lazy'' instance slot: define :if_needed [declaration] name ( variable : class-name ); procedure-body enddefine Adds access and update methods to the generic procedure name which behave very much like ordinary slot methods defined on class-name, except that the initial value of the slot is not computed each time an instance is created, but only when the slot is first accessed; the value is computed by evaluating procedure-body with the variable bound to the instance being accessed. An if_needed method uses a property to map instances to their ``slot'' values: it does not add a new instance slot. 3.3 Method Wrappers -------------------- A method wrapper is a closure around a particular method part which can enhance or modify the behaviour of that method. define_wrapper [define_form syntax] Adds a wrapper to an existing method definition: define :wrapper [updaterof] name ( [typed-input-locals] variable ) [output-locals] [with-list] ; procedure-body enddefine The typed-input-locals are the same as for define_method and declare a set of input arguments with optional class constraints. For a wrapper definition to be valid, there must already exist a method with the same name and argument constraints (signature). The effect of the definition is to replace that method with a closure of the wrapper procedure, with the additional variable bound to the original method definition. When the method is dispatched it is the wrapper which gets called, and the wrapper procedure can choose when -- or indeed, if -- to call the original method. The wrapped method can be a slot or shared-slot method as well as one created by define_method. A common use of wrappers is, in fact, to augment slot methods with extra access or update code. For example, the slot rest defined on extant class pair above can be modified to accept only list values: define :wrapper updaterof rest(x, p:pair, upd_p); unless x.isList then mishap('LIST NEEDED', [^x]); endunless; ;;; do the update upd_p(x, p); enddefine; You can have at most one wrapper attached to any particular method. A second or subsequent wrapper definition simply replaces the previous version. wrapper_deref(method) -> method [procedure] Obsolete procedure for stripping any wrappers from a method to obtain the innermost "kernel" procedure. The new wrapper variable wrapper_kernel is the recommended way to get a handle on the kernel procedure. wrapper_invoker [protected variable] This variable is automatically bound to the generic procedure to which the wrapper is attached. The anticipated main use is for "on update" wrappers whereby it is possible to determine which slot is responsible for the invocation. wrapper_kernel [protected variable] This variable is automatically bound to the inner "kernel" procedure. The anticipated main use is to debug wrappers. Using the wrapper_kernel it is possible to invoke the underlying action but bypass all earlier/later wrappers. 3.4 Method Chaining -------------------- Method chaining is relevant in applications of generic procedures where more than one method is applicable. Recall that in this case the applicable methods are sorted into order, and normally only the first one is applied. But through call_next_method, that method can call the next one in the sequence, which may in turn call the method following it and so on, so that the initial call can lead to a chain of method calls. Method chaining is used most often when a derived class needs to add code incrementally to its superclass methods, a process known as specialisation: the superclass method code can be used directly -- i.e. without having to be duplicated -- but with refinements added appropriate to the derived class. For example: define :method report(x:CompoundItem); ;;; print standard report call_next_method(x); ;;; add additional information printf('Parts:'); applist(x.itemParts, itemPartNo<>printf(%'\t%p\n'%)); enddefine; define :instance grommet:CompoundItem; itemPartNo = 'YYY-62-6'; itemDescription = 'Mini Grommet'; itemParts = [% instance Item; itemPartNo = 'ZZZ-18-0'; endinstance; instance Item; itemPartNo = 'ZZZ-19-3'; endinstance; %]; enddefine; report(grommet); Item: YYY-62-6 Mini Grommet Parts: ZZZ-18-0 ZZZ-19-3 call_next_method(expr1, ..., exprN) [syntax] Allowed only inside a method definition, this calls the next method from the sorted list of methods applicable to the current call. If the current method is the last such method, the call will mishap. Note that although you can pass arbitrary arguments to call_next_method, there is no check that these are valid for the next method to be called: most often, you should pass exactly the same arguments as were supplied to the current call. call_all_next_methods(expr1, ..., exprN) [syntax] Allowed only inside a method definition, this calls all the remaining methods from the sorted list of methods applicable to the current call. The values of expr1,...,exprN are saved in a list and exploded onto the stack for each subsequent method invocation. As with call_next_method, it is your responsibility to ensure that the arguments are valid for all remaining methods. input_locals [syntax] -> input_locals output_locals [syntax] -> output_locals Allowed only inside a method definition, these will push or pop the input or output arguments of the enclosing method. They are designed particularly for use with call_next_method through the idiom call_next_method(input_locals) -> output_locals; 3.5 Operations on Generic Procedures ------------------------------------- isgeneric(item) -> bool [procedure] Returns if item is a generic procedure and otherwise. Note that traced generics AREN'T generics: they are closures of systrace. Therefore if you use isgeneric in your program, be aware that tracing generic procedures may cause your program to behave differently. will_apply_to(item1, ..., itemN, generic) [procedure] -> item1, ..., itemN, bool Determines whether a generic procedure has any applicable methods for arguments item1,..., itemN. Returns if so, or if the application of the generic procedure to those arguments would fail. The arguments are left untouched on the stack; this leads to the following idiom for using will_apply_to to determine which of generic procedures P, Q, and R to apply to arguments X1, X2, X3 and X4: apply( X1, X2, X3, X4, will_apply_to(P) and P or will_apply_to(Q) and Q or R ) fail_safe(item_1, item_2, ..., item_n, generic) -> bool [procedure] Applies a generic procedure in such a way that if the call fails, the result is , otherwise it is . A successful call may return additional results. For example: rest(99) => ;;; MISHAP - Method "rest" failed ;;; INVOLVING: 99 fail_safe(99, rest) => ** fail_safe([1 2 3], rest) => ** [2 3] ... Tracing Methods -------------------- The standard Pop-11 trace mechanism will work with generic procedures but will only show calls to the generic procedure itself. Using the objectclass trace_method form and its variants reveals the signatures of the particular methods being called. trace_method generic_1, generic_2, ..., generic__n; [syntax] Traces the named generic procedures so that each call displays the particular method applied with its arguments and results. Uses systrace_proc for display, so that the output is similar to that of a normal trace (see HELP * TRACE). untrace_method generic_1, generic_2, ..., generic__n; [syntax] Removes tracing from the named generic procedures. trace_every_method [syntax] Adds tracing to all generic procedures currently defined. untrace_every_method [syntax] Removes tracing from all generic procedures. ... Cancelling Methods ----------------------- cancel_method signature, ...; [syntax] cancel_method name, ...; The first form cancels a method (or methods) with the specified signature(s); the second form cancels all methods with the given name(s). Cancelling a method means deleting it from the generic procedure of the same name. Cancelling all the methods leaves a generic procedure without any methods so that calling it is bound to fail. For example: /* cancel the report method on Items */ cancel_method report(Item); /* cancel all report methods */ cancel_method report; In a method signature, use the word "_" to denote the default method for an argument, e.g. cancel_method isPartOf(_, CompoundItem); method_path(classes, generic) -> method [procedure] method -> method_path(classes, generic) Returns or updates the method associated with a list of classes in a generic procedure. method_path returns if there is no such method defined; assigning to the updater will delete a method. 3.6 Pre-Defined Generic Procedures ----------------------------------- Default methods are defined by objectclass for all these procedures. print_instance(object) [generic procedure] Called whenever an objectclass instance is printed by syspr. Define a method for this to change the way instances are displayed. Example: define :class Term; slot termFunctor, termArgs == []; enddefine; define :method print_instance(t:Term); printf('%p(', [% t.termFunctor %]); lvars args; for args on t.termArgs do pr(args.hd); unless null(args.tl) then printf(', ') endunless; endfor; printf(')'); enddefine; vars term = consTerm("+", [3 4]); term => ** +(3, 4) See the section on Printing Objects above. apply_instance(object) [generic procedure] The default class_apply of all objectclass classes. Define a method for this if you want to change the way instances are applied. For example: define :method apply_instance(t:Term); ;;; assume argument indexing t.termArgs(); enddefine; term(1) => ** 3 =_instance(object1, object2) -> bool [generic procedure] The default class_= of all classes created by objectclass. Define a method for this if you want to change the way instances are compared for equality. For example: define :method =_instance(t1:Term, t2:Term); ;;; force terms to be unique t1 == t2; enddefine; term = term => ** term = copy(term) => ** The default method calls sys_= which tests for structural equality. See REF * DATA. hash_instance(object) -> int [generic procedure] The default class_hash of all classes created by objectclass. Define a method for this if you want to change the way instances are hashed, e.g. for property lookup (see REF * PROPS). For example: define :method hash_instance(t:Term); syshash(t.termFunctor) + t.termArgs.length; enddefine; Note that if you redefine =_instance and/or hash_instance for any class, you should always ensure that, for any x and y, if =_instance(x, y) then hash_instance(x) == hash_instance(y) fail_generic(object, generic) [generic procedure variable] Called when the application of a generic to an object has failed because there is no method applicable to the object. The default method causes a mishap. You can add methods to this if you want to change the way failure behaves for particular generics or classes. Note that the second argument will always be a generic procedure, and so cannot be sensibly constrained. Also, if the failed generic expects more than one argument, then the remainder will be on the stack below the object. ------------------------------------------ 4 Browsing Classes and Generic Procedures ------------------------------------------ ved_oc [ options ] [ pattern... ] [procedure] A basic class browser. Without arguments, this command produces a vertical listing of all the currently loaded classes. This listing can be expanded through the use of the following options. Note that a leading '+' selects an option and a leading '-' deselects an option. Options from the command line are used in combination with a default option string. Options are processed from left to right and later options override earlier ones. Attribute display options +c +children show the subclasses +p +parents show the parent classes +s +slots show the slots of the class +t +type show the type of class e.g. mixin ++ turn on all display options -- suppress all display options Filter options +a +ancestors expand to include ancestors +d +descendants expand to include descendants +i +ignored include ignored +l +leaf filter out classes with children +o +obsolete include obsolete classes +r +root filter out classes with parents Sort options +A +Alphabetical list alphabetically (default) +H +Hierarchical list by generality, most general first +R +Reverse reverse the list after sorting These options can be concatenated together. For example: oc +pRc would produce a vertical listing of all classes including both parent and child class information, listed in reverse alphabetical order. The list of classes displayed can be constrained by supplying one or more patterns (regular expressions): a class will be displayed only if its name matches any of the patterns. The +ancestors option causes all the ancestors of the classes to be included too. Similarly the +descendants option include the descendants of the named classes. For example, ENTER oc +td truck lorry produces a vertical listing of just the truck and lorry classes and all their subclasses, recursively, together with class type information. If no patterns are supplied then the variable vved_oc_implicit_classes is used to determine which classes to display. Construction of the class list proceeds as follows :- A complete list of classes known to the system is prepared. Any obsolete classes are immediately removed from the list, unless the +obsolete option has been used. [NB: obsolete classes are created when you recompile a class definition in such a way that the old key cannot be reused, or when pop_oc_reuse is . When this happens, a new key is allocated and the old key is marked as obsolete. Such obsolete keys are normally of no interest.] The list of patterns supplied on the command line (or vved_oc_implicit_classes) is then used to filter out a set of matching class names. Regardless of whether the list of classes is specified explicitly or implicitly, any classes filtered by vved_oc_ignore or vved_oc_ignore_list are then removed from the list. The +ignore option can be used to disable this stage of the filtering. Use ved_oci to add or delete patterns from vved_oc_ignore_list. This list of classes is then filtered according to the +root and +leaf options and finally expanded according to the +ancestors and +descendants options. ved_oca [ class ] [procedure variable] ved_oca_showtree [ class ] [procedure variable] Displays an indented list of the ancestors of a class. The basic version simply creates a text buffer. The "showtree" version uses lib showtree. ved_ocd [ class ] [procedure variable] ved_ocd_showtree [ class ] [procedure variable] Displays an indented list of the descendants of a class. The basic version simply creates a text buffer. The "showtree" version uses lib showtree. ved_ocg [ pattern... ] [procedure variable] Describes generic procedures whose names match any of the given patterns (regular expressions). ved_oci [procedure variable] ved_oci +pattern ved_oci -pattern Manipulates the vved_oc_ignore_list name filter. Given an explicit argument, ved_oci will add (+) or delete (-) a regular expression from the name filter. Without an argument, ved_oci is sensitive to whether it is used in the display buffer or not. If it is used in an ordinary buffer it simply displays the regular expressions in the filter. If it is used inside the display buffer it asks the user to choose between d take the current line and remove it from the ignored regexps r read the whole of the display buffer and create the ignore filter from that c cancel This is designed to enable quick configuration of the vved_oc_ignore_list name filter. vved_oc_ignore_list [variable] List of name filters used by ved_oc: class names recognised by filters in the list are excluded from the display, unless the ignore option is specified. The default value is the empty list, which excludes nothing. Regular expression filters can be added and removed from this list with the ved_oci command. vved_oc_ignore [variable] Name filter used by ved_oc: class names recognised by this filter are excluded from the display, unless the ignore option is specified. This variable is provided for complex filtering not handled by ved_oci. The default value is the empty list, which excludes nothing. vved_oc_implicit_classes [variable] Name filter used by ved_oc: if no explicit patterns are supplied on the command line, this filter is applied to all existing class names and any name recognised by the filter is included in the display (unless subsequently excluded by one of the ignore filters). The default value is the empty vector, which matches all names. vved_oc_defaults [variable] Default option string for ved_oc, implicitly prefixed to any options supplied on the command line (with an intervening space). Options are processed from left to right, so explicit options can override these defaults. The default value is the empty string (meaning no default options); a typical use of this variable would be '+tpcs' -> vved_oc_defaults; which would arrange that the type, parents, children and slots of classes would be displayed unless overridden on the command line, e.g. by doing oc -s to suppress display of slot information. 4.1 Name Filters ----------------- A name filter is responsible for recognising names. It can be any of the following data types: # A string or word, interpreted as a regular expression: a name is recognised if it matches the expression. The expression is implicitly constrained to match the whole name, so an ordinary string stands for itself; use @?@*string@?@* for a partial match on string. # A procedure: a name is recognised if the procedure returns non-false when applied to it. # A list of filters, interpreted as the union of its members: a name is recognised if it is recognised by any of the filters. # A vector of filters, interpreted as the conjunction of its members: a name is recognised if it is recognised by all the filters. ----------------------- 5 Objectclass and Popc ----------------------- You can compile an objectclass program into a stand-alone application with Popc subject to the following restrictions: (1) Only the syntax forms defined by the objectclass library are supported by Popc. Run-time operations such as class_construct, create_instance, will_apply_to, and most of the others described in this file, are simply not available in a stand-alone application. This is because all objectclass information is stripped from keys compiled into the application -- making them indistinguishable from ordinary Poplog keys -- and generic procedures are frozen into their linked form. (2) The whole program must be compiled as a single object file. You can still compile a program from several source files, but only if they are compiled together into one module using the '-g' option of Popc: popc -g program.w \( file1.p file2.p ... \) This is because poplink is unable to link generic procedures from method parts defined in separate modules. An unfortunate corollary of this restriction is that you cannot separately-compile library modules containing objectclass definitions. For an objectclass program to be compiled with Popc, it must include the line: uses-now objectclass; See HELP * POPC for general information about using Popc. --------------------------------------- 6 Objectclass Procedures and Variables --------------------------------------- optimise_objectclass(level) [procedure] Use this procedure to force relinking of generic procedures and is-predicates, and optionally to reclaim space used by the objectclass library. level can be a single keyword or a list of keywords indicating which optimisations to perform. The order of keywords is not important. The keyword "all" forces linking of all objectclass procedures. The keyword "irreversible" implies "all" but additionally instructs optimise_objectclass to destroy the run-time tables needed for linking of further methods. This is provided because creating new methods on the fly is a relatively uncommon operation in delivered applications. However, since it is such a potentially destructive operation, it checks to see whether or not you have loaded code which requires these tables; if it has, then "irreversible" is the same as "all". Since the syntax words made available by lib objectclass will no longer work, the "irreversible" keyword also disables these keywords. The keyword "unchecked" forces the destruction of the run-time tables without checking whether or not this is safe. Use this only if you definitely know what you are doing. The use of "irreversible" also causes methods to be marked as non-writeable. If you are familiar with sys_lock_system then you may well want to take advantage of this feature, too. Keyword Meaning ------- ------- all The same as [isas methods] delivery_time The same as all the other keywords with the exception of "unchecked" irreversible Irreversibly link methods, checking whether or not this is legal. This keyword automatically entails "all". Destroys the run-time tables and marks methods as non-writeable. isas Force the linking of all is-predicates. methods Force the linking of all generic procedures. unchecked Don't check whether it is safe to destroy the run-time tables. Just do it anyway. zap_methods Sets the pdprops of generic procedures to . This makes printing and debugging much harder. zap_method_parts Sets the pdprops of all method parts to . This only matters if you are using the pop debugger. zap_objectclass_section Cancels and zaps the objectclass section. Don't do this unless you have finished compiling. Auxiliary autoloaded utilities and libraries depend on the existence of this section. link_all_generics() [procedure] Forces all generic procedures to become linked. This is sometimes useful when you would like to distinguish the costs of method linking from that of the general program. Has the same effect as doing optimise_objectclass("methods") but may be quicker. pop_oc_reuse -> bool [variable] bool -> pop_oc_reuse Determines whether existing classes can be reused during recompilation of objectclass definitions. If , a key is never reused, and a new key is constructed every time you compile an objectclass definition using define_class or similar. If (the default), and you recompile an objectclass definition without modifying it in any way, the old class will be reused. pop_oc_sensitive_methods -> item [variable] item -> pop_oc_sensitive_methods Controls the way methods react when existing classes are recompiled and a new key is allocated. This happens when the class definition changes or when pop_oc_reuse is set to . It can have the following values Value Meaning ----- ------- ignore the new class "both" respond to both the old and new class "new" only respond to instances of the new class The default value is "new". pop_oc_trace -> item [variable] item -> pop_oc_trace Controls debugging messages printed by objectclass. It can have the following values: Value Meaning ----- ------- no tracing "cancel" trace cancelling or unprotecting identifiers "link" trace LINKING OF generic procedures "replace" trace objectclass replacement "upgrade" trace method upgrades for new keys [ ... ] trace the union of the flags trace everything The default value is . pop_oc_writeable_default -> item [variable] item -> pop_oc_writeable_default Determines the default writeable/nonwriteable attribute for new keys created by objectclass, when not specified explicitly in the class definition. It can have the following values: Value Meaning ----- ------- no default "writeable" writeable default "nonwriteable" nonwriteable default This is relevant when making saved images with sys_lock_system or creating stand-alone applications with Popc. The default value is "writeable". pop_oc_version -> int [constant] This is a simple integer defining the version of objectclass in use, in the form (major * 1000) + minor where major is the major version number and minor the minor version number within the former. --- C.all/lib/objectclass/ref/objectclass --- Copyright University of Sussex 2006. All rights reserved.