16
SKILL++ Object System
To gain benefits from object-oriented programming, the Cadence® SKILL language requires extensions beyond lexical scoping and persistent environments.
The Cadence SKILL++ Object System allows for object-oriented interfaces based on classes and generic functions composed of methods specialized on those classes. A class can inherit attributes and functionality from another class known as its superclass. SKILL++ class hierarchies result from this inheritance relationship.
To attain the maximum benefit from the SKILL++ Object System, you should only use it with lexical scoping, because lexical scoping magnifies the power of the interfaces you can develop with the SKILL++ Object System.
You do not need to be familiar with another object-oriented programming language or system to understand or use the SKILL++ Object System. However, if you are familiar with the Common Lisp Object System (CLOS), you can apply your experience of CLOS in learning the SKILL++ Object System as the SKILL++ Object System is modelled after a subset of the Common Lisp Object System.
For more information, see the following sections:
Basic Concepts
The following items are central concepts of the SKILL++ Object System:
For more information, see the following sections:
- Defining a Class (defclass)
- Instantiating a Class (makeInstance)
- Initializing an Instance (initializeInstance)
- Reading and Writing Instance Slots
- Defining a Generic Function (defgeneric)
- Defining a Method (defmethod)
Classes and Instances
A class is a data structure template. A specific application of the template is termed an instance. All instances of a class have the same slots. SKILL++ Object System provides the following functions:
-
defclassfunction to create a class -
makeInstancefunction to create an instance of a class -
initializeInstancefunction to initialize a newly created instance
Generic Functions and Methods
A generic function is a collection of function objects. Each element in the collection is called a method. Each method corresponds to a class. When you call a generic function, you pass an instance as the first argument. The SKILL++ Object System uses the class of the first argument to determine which methods to evaluate.
To distinguish them from SKILL++ Object System generic functions, SKILL functions are called simple functions. The SKILL++ Object System provides the following functions.
Subclasses and Superclasses
SKILL++ Object System supports both single and multiple inheritance. In single inheritance, one class B can inherit structure slots and methods from another class A. You can describe the relationship between the class A and class B as follows:
In multiple inheritance, class B can inherit structure slots and methods from multiple classes. For example, class A and class C. In this case, the relationship between class A, B, and C is as follows:
Class Precedence List
All inheritance decisions are governed by the class precedence list, which is an ordered list of a given class and its superclasses. The following rules determine the precedence order of classes:
-
In single inheritance: A class is always more specific than its superclass. So the order of precedence flows from left to right. For example,
defclass(A ())
defclass(B (A))
; order of precedence is from B to A
-
In multiple inheritance: For a given class, superclasses listed on the left are more specific than those listed on the right. So the order of precedence is from the superclass on the left to the superclass on the right. For example,
defclass(A () ())
defclass(B () ())
defclass(X (A B) ()) ; in class X, class A should come before class B
defclass(Y (B A) ()) ; in class Y, class B should come before class A
; as per the above rule, the following code results in an error:
defclass(M (X Y) ())
Defining a Class (defclass)
The domain of geometric objects provides good examples for using object oriented programming. Use the defclass function to define a class. You specify the superclass, if any, and all the slots of the class.
defclass( GeometricObject
() ;;; superclass
() ;;; list of slot descriptions
) ; defclass
This example defines the GeometricObject class. Defining the GeometricObject class allows the subsequent definition of default behavior of all geometric objects. It has no slots. Because no superclass is specified, the superclass is the standardObject class.
defclass( Triangle
( GeometricObject ) ;;; superclass
(
( x ) ;;; x slot description
( y ) ;;; y slot description
( z ) ;;; z slot description
)
) ; defClass
This example defines the Triangle class. It declares that
-
The
Triangleclass is a subclass of theGeometricObjectclass -
Each instance shall have three slots named
x,y, and z
Slot Options
Slot options, also known as slot specifiers, govern how you initialize the slot as well as your access to the slot.
| Slot Option | Value | Meaning |
|---|---|---|
|
Defines a keyword argument for the For more information on the order of precedence used when multiple @initargs are supplied, see Rules of Initialization. |
||
|
Defines a generic function with this name. The function returns the value of the slot. |
||
|
Defines a generic function with this name. The function accepts a single argument which becomes the new slot value. |
Example 1
defclass( Triangle
( GeometricObject )
(
( x
@initarg x
)
( y
@initarg y
)
( z
@initarg z
)
)
) ; defClass
Example 2
defclass( Circle
( GeometricObject )
(
( r @initarg r )
)
) ; defClass
Inheritance of Slots
The following rules govern the inheritance of slots in subclasses:
- If a subclass is inherited from a superclass, it also inherits the slots of the superclass.
-
If a subclass is inherited from multiple superclasses, which have slots with the same name, then only one slot of the given name is inherited. For example:
defclass( Z1 () ((a @initform 1) (b @initform 1))) defclass( Z2 () ((b @initform 2) (a @initform 2) (zz))) defclass( Z3 () ((b @initform 3) (a @initform 3) (zz))) defclass( Z4 () ((b @initform 4)) defclass( Z5 (Z1 Z2 Z3 Z4) ()) defclass( Z6 (Z4 Z3 Z2 Z1) ()) z5 = makeInstance('Z5 ) z5->? => (a b zz) ;; slots a, b, and zz are not duplicated
defclass(A () ((slotA) (slotB) (slotA @initform 42)))
*WARNING* duplicate slot slotA
Similarly, an error is raised if you define a class in which two @reader or @writer slot options have the same name. For example:
defclass(A () ((aa @reader getA) (ff @reader getA)))
*Error* defclass: slots (aa ff) cannot use the same name for @reader - getA
defclass(B () ((aa @reader getB) (dd @writer getB)))
*Error* defclass: slot aa cannot use the same name for @reader and @writer - getB
Rules of Initialization
-
If a subclass is inherited from a superclass, it also inherits the slots and methods of the superclass. When you instantiate such a subclass, the initialization arguments (
@initargs) of the subclass have precedence over the initialization arguments of the superclass. For example:defclass( A1 () ((slot @initform 1 @initarg a))) defclass( B1 (A1) ((slot @initform 2 @initarg b))) (slotValue (makeInstance 'B1 ?b 3) 'slot) => 3 (slotValue (makeInstance 'B1 ?a 3) 'slot) => 3 (slotValue (makeInstance 'B1 ?b 3 ?a 4) 'slot) => 3 (slotValue (makeInstance 'B1 ?a 3 ?b 4) 'slot) => 4
For more information on instantiating classes, see Instantiating a Class (makeInstance). -
If two or more
@initargsinitialize the same slot, then the order of precedence of the initialization arguments is from left to right. For example:defclass( C1 () ((slot1) (slot3 @initarg s3 @reader rs3) (slot4 @initarg s4 @writer ws4 @initarg (s4a 'slot34)))) (slotValue (makeInstance 'C1 ) 'slot4 ) => slot34 (slotValue (makeInstance 'C1 ?s4a 5 ) 'slot4 ) => 5 (slotValue (makeInstance 'C1 ?s4 5 ) 'slot4 ) => 5 (slotValue (makeInstance 'C1 ?s4 5 ?s4a 6 ) 'slot4 ) => 5 (slotValue (makeInstance 'C1 ?s4a 5 ?s4 6 ) 'slot4 ) => 6
Instantiating a Class (makeInstance)
Use the makeInstance function to instantiate a class. The first argument designates the class you are instantiating. Subsequent keyword arguments initialize the instance’s slots. The makeInstance function returns the newly allocated instance of the class.
procedure( makeTriangle( x y z )
if( x<y+z && y<z+x && z<x+y
then
makeInstance( 'Triangle
?x 1.0*x
?y 1.0*y
?z 1.0*z
)
else
error(
"%n %n %n fail triangle inequality test\n"
x y z
)
) ; if
) ; procedure
exampleTriangle = makeTriangle( 3 4 5 ) => stdobj:0x1e6030
The print representation for a SKILL++ Object System instance consists of stdobj: followed by a hexadecimal number.
makeInstance function does not check for invalid @initargs. If your code contains invalid @initargs, makeInstance accepts the @initargs as additional @rest options.Initializing an Instance (initializeInstance)
The initializeInstance function is called by makeInstance to initialize a newly created instance. You can define methods for initializeInstance to specify the actions that need to be taken when the instance is initialized. You can also use initializeInstance to add initialization parameters in addition to those defined by @initform.
defclass(A ()
(
(x @initarg x @initform 1)
(y @initarg y @initform 2)
(product)
)
)
defmethod( initializeInstance @after ((obj A) @key product @rest args)
if(product then
obj->product = product
else
obj->product = obj->x * obj->y
)
printf("initializeInstance : A : was called with args - obj == '%L'
product == '%L' rest == '%L'\n" obj product args)
printf(" object initialized to: %L\n" obj->??)
)
makeInstance('A)
initializeInstance : A : was called with args - obj == 'stdobj@0x2d61020' product == 'nil' rest == 'nil'
object initialized to: (x 1 y 2 product 2)
=> stdobj@0x2d61020
makeInstance('A ?x 5 ?y 10)
initializeInstance : A : was called with args - obj == 'stdobj@0x2d61038' product == 'nil' rest == '(?x 5 ?y 10)'
object initialized to: (x 5 y 10 product 50)
=> stdobj@0x2d61038
makeInstance('A ?product 30)
initializeInstance : A : was called with args - obj == 'stdobj@0x2d61050' product == '30' rest == 'nil'
object initialized to: (x 1 y 2 product 30)
=> stdobj@0x2d61050
Reading and Writing Instance Slots
You can use the arrow operator to read a slot’s value.
exampleTriangle->x => 3.0
exampleTriangle->y => 4.0
exampleTriangle->z => 5.0
You can use the -> operator on the left-side of an assignment statement.
exampleTriangle->x = 3.5
The ->?? expression returns a list of the slots and their values.
Another approach is to use the @reader and @writer slot options to define generic functions for reading and writing slots when you define the class.
defclass( Triangle
( GeometricObject )
(
( x
@initarg x
@reader get_x
@writer set_x
)
( y
@initarg y
@reader get_y
@writer set_y
)
( z
@initarg z
@reader get_z
@writer set_z
)
)
) ; defClass
exampleTriangle = makeTriangle( 3 4 5 ) => stdobj:0x1e603c get_y( exampleTriangle ) => 4.0 set_x( exampleTriangle 3.5 ) => 3.5
Defining a Generic Function (defgeneric)
Use the defgeneric function to define a generic function. The body of the generic function defines the default method for the generic function.
defgeneric( Perimeter ( geometricObject )
error( "Subclass responsibility\n" )
) ; defgeneric
This example indicates that relevant subclasses of the geometricObject class, such the polygon class, should have a Perimeter method. Although not strictly necessary to do so, defining a generic function before defining any methods for it has two advantages:
-
You can specify a default method
Using thedefgenericfunction gives you control over the default method. When you invoke a generic function that has no default method, the SKILL++ Object System raises an error. The following example illustrates calling a generic function which does not have a method defined for the specific argument.Perimeter( 3 ) *Error* (Default-method) generic:Perimeter class:fixnum
-
You can document the template argument list
In the absence of a generic function, the first method you define automatically declares the generic function. The method’s argument list becomes the template argument list.
You can also use the defgeneric function to associate a proxy class, called a generic function class, with the generic function. A proxy class is useful when defining customSpecializer methods for a particular class, or for defining dependency protocol where the methods are specialized on a particular generic function class. The basic need for an application specific proxy class is to be able to differentiate one group of generic functions from others in an application specific way.
By default, all generic functions are associated with class:ilGenericFunction. So, to be able to use a proxy class you need to inherit the class from class:ilGenericFunction as shown in the following example.
Example 1
defclass(niGF (ilGenericFunction) ()) ; generic class 'niGF
defgeneric(niTest (x y) ?genericFunctionClass niGF) ; generic function niTest is associated with class:niGF
In this example, niGF is the proxy class for the generic function, niTest. The instance of this class is created when either a generic function is defined (at defgeneric time) or when this class is accessed for the first time.
To retrieve a proxy instance from the generic function object, use the getGFproxy function as shown in the example below. The instance of the proxy object retrieved is stored in property list of generic function symbol.
Example 2
getGFproxy('niTest)
=> stdobj@0x83c0018
classOf(getGFproxy('niTest))
=> class:niGF
classOf(getGFproxy('printself)) ;; class of standard generic function (printself)
=> class:ilGenericFunction ;; default
getGFproxy('abc)
=> nil ;; non-existing generic function
In addition, you can inherit from a generic function proxy class by using the defclass function, which is in turn inherited from class:ilGenericFunction (or optionally from any other standardObject class).
Defining a Method (defmethod)
Use the defmethod function to define a method. You do not need to define the generic function before you define a method for it. When you invoke a generic function, the SKILL++ Object System chooses the method to run based on the class of the first argument you pass to the function.
-
If you use conventional syntax
defmethod( … )in place of the LISP syntax( defmethod … ), use white space to separate the method name from the argument list. -
You must specify the class of the method’s first argument to the
defmethodfunction. The first argument todefmethoduses the following syntax:( s_arg1 s_class )
Example 1
defmethod( Perimeter (( triangle Triangle ))
let( (
(x triangle->x)
(y triangle->y)
(z triangle->z)
)
x+y+z
) ; let
) ; defmethod
Perimeter( exampleTriangle ) => 12.0
This example defines a method named Perimeter. It is specialized on the Triangle class.
Example 2
defmethod( Perimeter (( c Circle ))
2*c->r*3.1415
) ; defmethod
This example defines a Circle class and defines the Perimeter method for the Circle class.
You can specify additional optional arguments while defining a method of a generic function by using the @rest option. For example:
defgeneric( myTest (x @rest _args))
; The following derived methods use additional @key and @optional arguments:
defmethod( myTest (x) 1)
=> t
defmethod( myTest ((x string) @key (z 2)) 3)
=> t
defmethod( myTest ((x number) @optional a b c) 4)
=> t
The eqv Specializer
While defining a method using defmethod, you can use the eqv specializer to specialize the method on objects other than classes (for example, some value of its arguments).
When eqv is encountered in a method declaration, the value of the argument of the method is compared to the eqv value. If a match is found, the method is excecuted. For example,
defgeneric( factorial (x))
defmethod( factorial ((x fixnum)) ;; #1
(times x (factorial (sub1 x))))
defmethod( factorial ((x (eqv 0)) ;; #2
1)
; method #2 is applicable if the argument is eqv to 0
Defining Method Combinations (@before, @after, and @around)
Once you have defined a generic function, you can combine it with methods that execute before or after the normal implementation. There are three kinds of additional or auxiliary methods that you can use with defmethod: @before, @after,and @around methods.
A standard method combination will have defmethod as the primary method and a method qualifier (@before, @after, @around) between the name of the method and the parameter list.
defmethod( mymethod @before ((x number)) )
defmethod( mymethod @after ((x fixnum)) )
defmethod( mymethod @around ((x number) y))
All the applicable methods are evaluated and partitioned into separate lists according to their qualifiers. A diagrammatic representation of the order in which the applicable methods are invoked is given below:

The detailed order in which the applicable methods are invoked is as follows:
-
The
@aroundmethods, if defined, are executed first.
If an@aroundmethod invokescallNextMethod, execute the next most specific@aroundmethod, with all itscallNextMethodmethods. -
If there are no
@aroundmethods or if an@aroundmethod invokescallNextMethodbut there are no further@aroundmethods to call then proceed as follows:-
The
@beforemethods are executed thereafter, with the most-specific method invoked first. All@beforemethods are called before any of the primary methods.
If you use acallNextMethodin a@beforemethod, an error returns. -
Then, the most-specific primary methods are called.
You can use acallNextMethodinside the body of a primary method to call the next most-specific primary method. -
The
@aftermethods are called after the primary methods but in the reverse order, with the least-specific method called first.
If you use acallNextMethodin an@aftermethod, an error returns.
-
The
If there is an error in a method, the execution returns to the nearest toplevel.
Example 1
defmethod( mymethod (( x fixnum)) ;; # 1 - primary method
...)
defmethod( mymethod @before (( x number )) ;; # 2
...)
defmethod( mymethod @before (( x fixnum )) ;; # 3
...)
defmethod( mymethod @after (( x fixnum )) ;; #4
...)
defmethod( mymethod @around (( x fixnum)) ;; # 5 - primary method
. . .
callNextMethod()
. . .)
When mymethod is invoked (with a fixnum argument), the methods are called in the following order:
#5 -> #3 -> #2 -> #1 -> #4
Example 2
defmethod(aTest @around ((x number) y)
/* 1 : around method*/
callNextMethod())
defmethod(aTest @before ((x number) y)
/* 2 : before method */
…)
defmethod(aTest ((x number) y)
/* 3 : a primary method */
callNextMethod())
defmethod(aTest @after ((x number) y)
/* 4 : after method */
…)
defmethod(aTest @around ((x systemObject) y)
/* 5 : another @around method */
callNextMethod())
aTest(1)
=> #1 -> #5 -> #2 -> #3 -> #4 (calling order)
Multi-Method Dispatch
SKILL++ supports multi-method dispatch. With multi-method dispatch, all arguments of a method are treated equally and the method to be applied is decided at runtime, based on the dynamically-determined types of arguments.
It means that you can define more than one method with the same name in your code. When the method call is made, instead of one parameter specializer determining the method to be applied, it is determined by multiple parameter specializers.
Example: Single-Dispatch
;; body of method1
;; method1 specialized on its first argument only (obj)
defmethod ( method1 ((obj string) x y)
) ; end of method1(string)
Example: Multiple-Dispatch
;; body of method2
;; method2 specialized on 3 arguments:
;; obj of type string
;; x of type number
;; y of class "classY"
defmethod ( method2 ((obj string) (x number) (y classY))
) ; end of method2
Method Specificity
In case, all applicable methods have the arguments of the same type, the methods are sorted on the order of specificity. So, the most-specific primary method is called first; other methods can then be called from the primary method by using the callNextMethod. For example,
defmethod( test ((x number) (y string))
printf("test number/string\n")
callNextMethod()
)
defmethod( test ((x fixnum) (y string))
printf("test fixnum/string\n")
callNextMethod()
)
defmethod( test ((x number) (y primitiveObject))
printf("test number/primitiveObject\n")
callNextMethod()
)
defmethod( test ((x fixnum) (y primitiveObject))
printf("test fixnum/primitiveObject\n")
callNextMethod()
)
defmethod( test ((x t) (y t))
printf("test t/t\n")
)
The class precedence list is used in determining the method specificity:
t -> systemObject -> primitiveObject -> string
t -> systemObject -> primitiveObject -> number -> fixnum
So, for test(1 "test") the order of method calls is:
; all the applicable methods are called according to the class precedence of arguments:
=> test fixnum/string
=> test fixnum/primitiveObject
=> test number/string
=> test number/primitiveObject
=> test t/t
For test(1.0 "test"), the order of method calls is:
;three applicable methods are called according to the class precedence of arguments:
=> test number/string
=> test number/primitiveObject
=> test t/t
Class Hierarchy
The diagram below is a horizontal view of the SKILL++ Object System class hierarchy.

-
tis the superclass of all classes. Classthas two immediate subclasses,standardObjectandsystemObject. -
standardObjectis the superclass of all classes you define withdefclassfunction. This is the primary portion of the class hierarchy that you can extend. -
systemObjectis the superclass ofprimitiveObjectandspecialObject. No subclasses ofsystemObjectcan be used withdefclass. -
primitiveObjectis the superclass of all SKILL built-in classes. -
specialObjectis the superclass of all classes corresponding to the C-level registrable “user-types.” -
defstructObjectis a superclass of all structures defined bydefstructfunction (You can create such a class withaddDefstructClassfunction).
This example shows how you can list all of the subclasses. Run this program to see what the class hierachy is at any given time.
procedure( getDirectSubclasses( classObject )
foreach( mapcar c subclassesOf( classObject )
className( c )
) ; foreach
) ; procedure
procedure( getAllSubclasses( classObject )
let( ( direct )
direct = getDirectSubclasses( classObject )
cons(
className( classObject )
direct && foreach( mapcar c direct
getAllSubclasses( findClass( c ))
) ; foreach
) ; cons
) ; let
) ; procedure
getAllSubclasses( findClass( 't )) =>
(t (standardObject
(GeometricObject
(Triangle)
(Point)
)
)
(systemObject
(primitiveObject
list()
(port)
(funobj)
(array)
(string)
(symbol)
(number
(fixnum)
(flonum)
)
)
( specialObject
(other)
(assocTable)
)
)
)
Browsing the Class Hierarchy
The SKILL++ Object System provides a number of functions for browsing the class hierarchy:
- Getting the Class Object from the Class Name
- Getting the Class Name from the Class Object
- Getting the Class of an Instance
- Getting the Class of the Environment Object (envObj)
- Getting the Superclasses of an Instance
- Checking if an Object Is an Instance of a Class
- Checking if One Class Is a Subclass of Another
Examples in these sections refer to the following code:
defclass( GeometricObject
() ;;; superclass
() ;;; list of slot descriptions
) ; defclass
defclass( Triangle
( GeometricObject ) ;;; superclass
(
( x @initarg x ) ;; x slot description
( y @initarg y ) ;; y slot description
( z @initarg z ) ;; z slot description
)
) ; defClass
exampleTriangle = makeTriangle( 3 4 5 ) => stdobj:0x1e6030
Getting the Class Object from the Class Name
Use the findClass function to get the class object from its name. Use a SKILL symbol to represent the class name.
findClass( 'Triangle ) => funobj:0x1cb2d8
Getting the Class Name from the Class Object
Use the className function to get the class symbol. The term class symbol refers to the symbol used to represent the class name. The SKILL++ Object System uses a SKILL symbol to represent the class name.
className( findClass( 'Triangle )) => Triangle
Getting the Class of an Instance
Use the classOf function to get the class of an instance.
className( classOf( exampleTriangle ) ) => Triangle
Getting the Class of the Environment Object (envObj)
Use the classOf function to get the class of the environment object envObj.
z = let( (( x 3 ))
theEnvironment()
) ; let
=> envobj:0x1e0060
classOf(z)
=> class:envobj
Getting the Superclasses of an Instance
Use the superclassesOf function to get the superclasses of a class. The function returns a list of class objects.
L = superclassesOf( classOf( exampleTriangle ) )
foreach( mapcar classObject L className( classObject )
) ; foreach
=> (Triangle GeometricObject standardObject t)
Checking if an Object Is an Instance of a Class
Use the classp function to check if an object is an instance of a class. You can pass either the class symbol or the class object as the second argument.
Example 1
classp( exampleTriangle 'Triangle ) => t
classp( 5 'fixnum ) => t
classp( 5 'Triangle ) => nil
5 is a fixnum. 5 is not an instance of Triangle.
Example 2
classp( exampleTriangle 'GeometricObject ) => t
classp( exampleTriangle 'standardObject ) => t
classp( exampleTriangle t ) => t
This example illustrates that classp returns t for all superclasses of the class of an instance. Triangle is a subclass of GeometricObject. GeometricObject is a subclass of standardObject. standardObject is a subclass of t.
Checking if One Class Is a Subclass of Another
Use the subclassp function to determine whether one class is a subclass of another.
Example 1
subclassp(
findClass( 'Triangle )
findClass( 'GeometricObject )
) => t
Triangle is a subclass of GeometricObject.
Example 2
subclassp(
findClass( 'Triangle )
findClass( t )
) => t
Example 3
subclassp(
findClass( 'Triangle )
findClass( 'fixnum )
) => nil
Triangle is not a subclass of fixnum.
Advanced Concepts
This section covers the following advanced aspects of the SKILL++ Object System:
- Method Argument Restrictions
- Applying a Generic Function
- Incremental Development
- Methods and Slots
- Sharing Private Functions and Data Between Methods
- Using Custom Specializers in SKILL++ Methods
Method Argument Restrictions
Method argument lists have the following restrictions:
Example
(defmethod test ((x class1) (y class2) @key a @rest _rest) ... )
(defmethod test ((x class3) (y class2) @key b @rest _rest) ... )
(defmethod test ((x class1) y @rest _rest) ... )
(defmethod test (x y @rest _rest) ... )
In the example above, the method test() can be called with or without @key arguments, provided at least two required arguments are passed to test().
Applying a Generic Function
When you apply a generic function to some arguments, the SKILL++ Object System performs the following actions to complete the function call. This process is called method dispatching. The SKILL++ Object System
- Retrieves the methods of the generic function.
-
Determines the class of the first argument to the generic function.
Based on the class of the first argument passed to the generic function, the SKILL++ Object System finds-
No applicable methods
SKILL++ Object System calls the default method for the generic function if one exists. Otherwise it signals an error. - Only one applicable method
-
More than one applicable method
This situation occurs when you have methods specialized on one or more superclasses of the first argument’s class.
-
No applicable methods
-
Determines applicable methods by examining the method’s class specializer.
A method is applicable if it specialized on the class of the first argument or a superclass of the class of the first argument. - Sorts the applicable methods according to the chain of superclasses of the first argument’s class.
- Calls the first method.
You can invoke the callNextMethod function from within a method to access the next applicable method in the ordering. For example:
defgeneric( describe (obj) ())
defclass( GeometricObject () () ) ; no slots or superclasses
defclass( Point ( GeometricObject )
(
( name @initarg name )
( x @initarg x );;; x-coordinate
( y @initarg y );;; y-coordinate
)
) ; defclass
defmethod( describe (( object GeometricObject )) className( classOf( object ))
) ; defmethod
defmethod( describe (( p Point )) sprintf( nil "%s %s @ %n:%n"
callNextMethod( p )
p->name
p->x
p->y
)
) ; the most specific method
aPoint = makeInstance( 'Point ?name "A" ?x 1 ?y 0 ) describe( aPoint ) => "Point A @ 1:0"
In the example, the describe generic function has two methods that are applicable to the argument aPoint:
The method specializing on the Point class is the more specific method, therefore the SKILL++ Object System applies the most specific method to the argument.
Incremental Development
In the SKILL++ environment , you can redefine SKILL++ functions incrementally. You should observe the following guidelines when redefining SKILL++ Object System elements of your application:
-
Redefining methods
During development, you can expect to redefine methods about as frequently as you redefine procedures. You can redefine a method as long as the redefined method’s argument list continues to conform to the generic function. -
Redefining generic functions
You need to redefine a generic function to change the generic function’s default method or argument list. Such need occurs infrequently. When you redefine a generic function, the SKILL++ Object System discards all existing methods for the generic function. -
Redefining classes
You need to redefine a class when you want to
If you need to redefine a class, you should exit the SKILL++ environment and reload you application. A frequent need to redefine classes probably indicates that you should analyze your application before further programming.
Methods and Slots
Methods are usually more expensive to use compared to slots but they offer data hiding and safety. Consider whether the Triangle’s Area method should access a slot containing the (precomputed) area or whether the area should be computed. The nature of your application dictates your final decision.
Computing the area may be costly if, for example, the area of triangles is used often. In such a situation, it would be more advantageous to add a slot for area to the triangle class. But then we would have to add @writer methods for the sides of a triangle to recalculate the area when the length of a side changes.
Sharing Private Functions and Data Between Methods
Using lexical scoping with the SKILL++ Object System allows all methods specialized on a class to share private functions and data.
The methods for a class might need access to data, such as an association table, that is shared between all instances of the class. Slots you specify in the defclass declaration are allocated within each instance of the class.
The methods for a class might all rely on certain helper functions which you need to make private.
Using the following template as a guide achieves both goals.
defgeneric( Fun1 ( obj … ) … )
defgeneric( Fun2 ( obj … ) … )
defclass( Example ( … ) ( … ))
let(
(
( classVar1 … ) ;data shared between all
( classVar2 … ) …. ) ;instances of the class
)
procedure( HelpFun1( …. ) ;private helper functions
procedure( HelpFun2( …. )
…
defmethod( Fun1 (( obj Example) …. )
defmethod( Fun2 (( obj Example ) … )
….
) ; let
Using Custom Specializers in SKILL++ Methods
In the SKILL++ object system you can define generic functions which are a collection of methods specialized on the class of one or more arguments passed to the generic function. This allows you to simply implement polymorphism in SKILL - in other words you can have multiple implementations of the same function which are focused on different objects, and this can make the code easier to maintain and develop as it avoids having to have a large cond or case function call within your code to switch between different objects; in addition the method dispatch will call the most applicable method first, with potentially that method being able to call the next most applicable method and so on.
The basic approach allows you to specialize on the class of the object passed to the function. For example:
defgeneric(CCScomputeArea (obj) error("Unrecognized object %L\n" obj)
)
defmethod(CCScomputeArea ((obj CCSrect))
; calculate area of a rectangle object
)
defmethod(CCScomputeArea ((obj CCScircle))
; calculate area of a circle
)
defmethod(CCScolor ((obj CCSshape))
; return the color of a shape (CCSrect and CCScircle are subclasses of CCSshape)
)
The CCSshape, CCScircle, and CCSrect are class names here that would have been defined elsewhere. This is an incomplete example just showing the methods so as to show the principle.
Sometimes you might have an object but want to further distinguish which method to call based on the value of a slot of that object. You can then use method dispatch to separately perform some work based on a slot or attribute value. One example might be that you have a database object (dbobject) in Virtuoso, which could represent a figure in the design (e.g. an instance, or a shape of some sort). The dbobjects in Virtuoso do have distinct objects for each kind of object - the objType attribute specifies whether the object is a "rect", a "path", a "net", a "cellView" and so on. However, because these are not instances of SKILL++ classes, there is limited ability to write methods specializing on those specific objects. You can write a method using a class name corresponding to the type of the object:
defmethod(CCScomputeArea ((obj dbobject))
; handle all the different kinds of database object within this method and compute the area appropriately
)
Normally, you cannot further refine the method to specialize on the object solely if it is a rect, or solely if it is a pathSeg, for example. This is because there are no subclasses of dbobject and so, bydefault, you cannot specialize on the objType of the database object.
What you would like to do is to use polymorphism to specialize on the objType of the database object in order to compute the area and perimeter of a variety of figure types.
Introduction
In addition to specializing on classes, SKILL++ allows specializing either on the value of a basic type, using the built-in eqv specializer, or by defining custom specializers to match specific objects. In order to enable these custom specializers, it is necessary to define a class plus three different methods to support the capability.
Before using custom specializers, you should consider whether this is the right mechanism for what you are trying to achieve. The danger is that you can end up hiding the logic of your program in the method dispatch, and ending up with methods specialized on particular values distributed across multiple files and this can make it hard to understand the flow of the program. You may be better off using a case or cond function within your code to group all the logic about different values in one place. One of the key benefits of declaring a method specialized on a class (the standard mechanism) is that the method also applies to subclasses too, unless a more specific method is defined. With custom specializers, the method can only be applied to specific matching instances, and if your custom specializers are complex they can end up with an order dependency which is very hard to reason about. It is possible to resolve any order dependency by defining the ilSpecMoreSpecificp method to determine the order, but again this adds additional complexity which may be unobvious to a reader of the code (this article does not use ilSpecMoreSpecificp).
Defining Methods Using Non-class Specializers
First of all, let us describe how you would declare a method using these specializers. As was seen above, the argument for a method normally is of the form (obj className) to make it specific to an instance of that class (or any of the children of that class). In order to use the custom specializers (or the eqv specializer), you would use the form (obj (specializerSymbol arg1 [arg2 ... argN])). The specializer symbol can be either eqv or one you define yourself - as will be outlined below. If eqv is used, then a single argument is given, and then the object will be matched against that value. So for example:
defmethod(CCStranslate ((str string)) strcat("EN: " str)) defmethod(CCStranslate ((str (eqv "hello"))) "bonjour") CCStranslate("goodbye") => "EN: goodbye" CCStranslate("hello") => "bonjour"
With the eqv specializer, it only makes sense to give a single argument (the other arguments are ignored), but with custom specializers you can specify multiple arguments to aid the specificity of the match. Note that with the eqv specializer, the argument is evaluated when the defmethod is defined (and so you must quote symbols if the argument is a symbol), but for custom specializers they are unevaluated and can only be literal values.
Using Custom Specializers With defmethod
For custom specializers, you need to define a class that is used to compare an argument to see if it matches the specializer, plus three methods: ilGenerateSpecializer, ilEquivalentSpecializers and ilArgMatchesSpecializer. The following example shows how this is done for a specializer designed to match the objType of the database object. This means that you can then use methods similar to the one given below:
defmethod(abFigArea ((obj (abDbFig "rect")))
abAreaOfBBox(obj~>bBox)
)
defmethod(abFigArea ((obj (abDbFig "donut")))
let((inner outer)
outer=abFigMaths.PI*obj~>outerRadius**2 inner=abFigMaths.PI*obj~>innerRadius**2
outer-inner
)
)
Defining a class for the specializer
In this case, the symbol abDbFig has been used for the specializer. The ilGenerateSpecializer method takes care of recognizing this symbol and generating an instance of the specializer class. The specializer class in this case is defined as follows:
defclass(abDbFigSpecializer ()
(
(objType @initarg objType)
)
)
The objType slot in the class is used to store the value of objType that the method is trying to match against.
ilGenerateSpecializer
The ilGenerateSpecializer method is called whenever a custom specializer is used in a defmethod as in the above example. It can either return the code (the SKILL expression) needed to generate the instance, or can directly return the instance of the specializer class. Three arguments are specified for it:
ilGenerateSpecializer(g_genericFunctionObj s_specSymbol l_argList)
An example of ilGenerateSpecializer using an eqv specializer to match the expected symbol. Since the generic function object is not used, it is prefixed with an underscore to prevent any impact on the SKILL Lint score and to indicate that it is unused.
defmethod(ilGenerateSpecializer (_gf (_spec (eqv 'abDbFig)) args) makeInstance('abDbFigSpecializer ?objType car(args))
Another way of implementing this is to return the code needed to generate the instance:
defmethod(ilGenerateSpecializer (_gf (_spec (eqv 'abDbFig)) args)
`makeInstance('abDbFigSpecializer ?objType ,car(args))
)
ilEquivalentSpecializer
The ilEquivalentSpecializer method is called whenever more than one method is defined for a generic function. It is intended to identify when identical custom specializers have been specified in the case of method redefinition, so that the new method replaces the previously defined one, rather than adding a new method. Having two methods with identical specializers would lead to errors during dispatch, which is why this method must be defined. The arguments signature is:
ilEquivalentSpecializer(g_genericFunctionObj g_SpecObj1 g_specObj2j)
| Argument | Description |
|---|---|
|
An instance of the ilGenericFunction class or a proxy class derived from it. (See ilGenerateSpecializer above) |
|
An example of ilEquivalentSpecializer to compare that the objType is identical for two instances, needed for method redefinition:
defmethod(ilEquivalentSpecializers (_gf (spec1 abDbFigSpecializer)
(spec2 abDbFigSpecializer))
spec1->objType==spec2->objType
)
ilArgMatchesSpecializer
Finally the ilArgMatchesSpecializer method is needed to determine whether a argument passed to the generic function at run time matches a particular specializer class, so that it then knows which method should be called. This has this argument signature ilArgMatchesSpecializer.
(g_genericFuncObj g_specObj g_argument)
| Argument | Description |
|---|---|
|
An instance of the ilGenericFunction class or a proxy class derived from it. (See ilGenerateSpecializer above) |
|
|
An instance of the specializer class being compared against. |
|
The method should check the type/class of the argument being compared to prevent errors if a candidate argument doesn't have the slots being accessed within the ilArgMatchesSpecializer. This could be done by adding a class specializer on the third argument, or including the type/class check as part of the check within the method itself.
defmethod(ilArgMatchesSpecializer (_gf (spec abDbFigSpecializer) (arg dbobject))
spec->objType==arg->objType
)
Return to top