Product Documentation
Cadence SKILL Language User Guide
Product Version IC23.1, September 2023

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:

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:

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:

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

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

@initarg

symbol

Defines a keyword argument for the makeInstance function.

You can supply more than one @initarg for a given slot.

For more information on the order of precedence used when multiple @initargs are supplied, see Rules of Initialization.

@initform

expression

Defines an expression which initializes the slot.

@reader

symbol

Defines a generic function with this name. The function returns the value of the slot.

@writer

symbol

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 you define a class with two slots that have the same name, SKILL creates the class but also issues a warning.
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

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

This lazy creation of the proxy object is especially true for generic functions loaded from a context.

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.

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:

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.

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:

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

Triangle is a subclass of 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

Method argument lists have the following restrictions:

Aspect Restriction

Number of arguments

The methods of a generic function can have additonal @optional arguments.

@rest arguments

All methods of a generic function must take @rest arguments if any of the methods take @rest arguments.

Keyword arguments

Each method of a generic function must

  • Take @rest arguments if any of the methods take @rest arguments
  • Allow a superset of the keyword arguments specified in the defgeneric declaration

@rest picks up all keyword arguments that have no matching keyword in the formal argument list. Different methods may have different default forms for the optional arguments and may accept different set of keywords.

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

  1. Retrieves the methods of the generic function.
  2. 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.
  3. 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.
  4. Sorts the applicable methods according to the chain of superclasses of the first argument’s class.
    • The first method in the ordering is the most specific method.
    • The last method in the ordering is the least specific method.
  5. 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:

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)

Argument Description

g_genericFunctionObj

An instance of the ilGenericFunction class or a proxy class derived from it. Using proxy classes is one way of associating custom specializers with generic functions. The SKILL Language User Guide describes how to define proxy classes to define a group of generic functions to use common specializers. A cut-down example of using a proxy class is given at the bottom of this article to compare with the preferred method.

s_specSymbol

The symbol used as the first entry in defmethod to trigger the custom specializer (abDbId in the example above). Note that this does not strictly need to be a symbol.

l_argList

The remaining arguments to the specializer - ("rect") in the example above. If more than one argument is given, this is a list of all the arguments.

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

g_genericFunctionObj

An instance of the ilGenericFunction class or a proxy class derived from it. (See ilGenerateSpecializer above)

g_specObj1

An instance of the specializer class to be compared.

g_specObj2

The second instance of the specializer class to be compared

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

g_genericFunctionObj

An instance of the ilGenericFunction class or a proxy class derived from it. (See ilGenerateSpecializer above)

g_specObj

An instance of the specializer class being compared against.

g_argument

The argument to the method that is being compared.

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.

For example:

defmethod(ilArgMatchesSpecializer (_gf (spec abDbFigSpecializer) (arg dbobject)) 
 spec->objType==arg->objType
)

Return to top
 ⠀
X