Package Elements^™

Michal Mnuk and Gerd Baumann
July 2002

Version: 0.3

Introduction

The package Elements implements some basic concepts found in object oriented programming. It is based on the notion of a class which is a structure containing variables and methods and which mimics classes from object oriented languages like C++ or Java. However, this package was not motivated by the wish to have an implementation of these languages in Mathematica, but primarily by the need to have hierarchically organized structures encapsulating data which could be used to facilitate and speed up the process of modeling and working with physical objects (elements) - that is where the name comes from. Hence, Elements contain only those concepts which serve this particular goal. Since there is a substantial overlap in its goals and those of the oriented programming, the package can be used in a variety of settings.

Classes and Objects

The main purpose of classes and objects is to encapsulate data so that they are independent of the environment and do not interfere with it. A class is a structure containing two types of data - properties and methods. Properties are intended to describe the static features of a class, methods reflect its dynamic behavior. Properties and methods are associated with attributes which can be used to describe additional aspects.
Classes serve as templates for creating objects. Once instantiated, classes cannot be changed. In contrast, objects can set the values of properties and thus influence their behavior.

Classes

Every class is described by the following items:

name unique identifier of the class
baseclass parent class from which all properties and methods are inherited
properties variables storing values
methods functions to be called by objects or classes themselves

The package Elements provides an initial class called Element which can be referred to by

Class["Element"]

- Class Element -

Creating New Classes

New classes are created by calling the function Class.

Class[name, baseclass, properties, methods] create a new class name and return a reference to it

A class can be referred to either by the object returned by the function Class or by Class[name] with the name of the class as the argument.
The first parameter name is an arbitrary string and specifies the name of the new class. It must be unique. The system will not allow declaring a new class with a name that is already used.
Every class has to be derived from an already existing one — specified in the second argument baseclass. A derived class inherits all properties and methods from its base class.

Properties

When a class is created, it may declare properties and their initial values. Propeties are essentially variables. Every object of that class will contain a private copy of all properties. In addition, properties can be associated with attributes carrying auxiliary information. Attributes are optional and, in general, it is the responsibility of the class to handle them. However, there are special attributes which are dealt with by the system.
A property declaration may have two forms:

symbol = expr declare a property symbol and set its initial value to expr ; expr is evaluated during the class declaration
symbol := expr declare a property symbol and set its initial value to expr ; expr is evaluated every time a new object of this class is created

In order to keep  the notation concise, properties are declared and referenced using symbols (although a string would be more appropriate as a specifier).

• An internal reference to a property is built from the string SymbolName[symbol]. This implies that symbol_1 and symbol_2 specify the same property if and only if SymbolName[symbol_1]≡SymbolName[symbol_2]. Especially, the context in which a symbol appears is not important.
• Even though properties are specified by symbols which may have assigned values, the system ensures that these values do not interfere with internals of classes and objects.

A property may have any number of attributes associated with it. They are specified in the standard Mathematica notation as rules.

attname → val declare a property attribute attname with value val

There are no restrictions imposed on names or values of attributes. They may be arbitraty Mathematica expressions, and are evaluated when the corresponding property is declared.
A declaration of a property with attributes has the form:

{symbol = expr, {att_1 val_1, att_2  val_2,…}}
{
symbol := expr, {att_1 val_1, att_2  val_2,…}}

A list with property declarations is given as the third argument in a class declaration.

We declare a new class derived from Element having two properties a and d.

C1 = Class["C1", Class["Element"], (* properties *) {a = 1 ... , Description  "Diameter"} }, (* methods *) {} ]

- Class C1 -

The properties of C1 may be obtained by calling the function GetProperties[C1].

GetProperties[C1]

{{a, 1}, {d, π}}

The package provides also the dot-notation known from standard object oriented languages.

C1 . a

1

C1 . d

π

This notation conflicts with the Dot function used to compute products of vectors and matrices. However, the datatypes relevant for Dot and for the package Elements do not intersect. Hence, the change of the behavior of Dot caused by Elements should not cause any confusion.

The properties are designed to be variable and objects may set them to arbitrary, but essentially constant, values. This means that the values are either constants like 3.4, or they may reference objects from the current Mathematica session, but not other properties. Note that in the declaration {a = 1, b = a + 1} the a in the value of b refers not to the property a but to the global variable a in the current Mathematica session.
There is a special, implicitly declared, property this which serves as a reference to the object in that it is used.

o1 = C1 . new[] ; o1  o1 . this

True

Methods

Methods are usual functions which can refer to properties or other methods. They are declared in the usual way.

f[args] = rhs define a method f for arguments args ;
f[args] := rsh the right hand side is evaluated according to usual rules

Even though methods are similar to other Mathematica functions, there are some important differences.

• The name of the declared method is determined as Head[f[args]]. This call must return a symbol. That is, declarations like HoldPattern[f[x_*y_]]:=2x are not allowed in class declaration.
• Unlike the standard evaluation procedure used in Mathematica, the head f  of the left hand side will not be evaluated when f is defined.
• Similarly as with property declarations, same symbols in different contexts define the same method.
• It is possible to have several declarations for the same method f which differ only in the signature. Usually, Mathematica tries more specific declarations before the general ones. Methods declared in classes will be, generally, used in the order of their declaration. This means, there are method declarations that would automatically get reordered in a Mathematica session, but in Elements their order remains unchanged. In most cases, no reordering is performed.

Methods are local to classes where they were declared and do not interfere with global values. If several declarations for the same method are encountered, they are dealt with in the standard way - depending on the signature, new declarations overwrite or extend the existing ones.
Every method may be associated with attributes. The syntax of an attribute declaration is the same as for properties. Hence, a method declaration with attributes has the form:

{f[args] = rhs, {att_1 val_1, att_2  val_2,…}}
{
f[args] := rhs, {att_1 val_1, att_2  val_2,…}}

A list with method declarations is given as the third argument in a class declaration.

The class C2 introduces a method named f with two signatures.

C2 = Class["C2", Class["Element"],  {a = 2, b = π},   ... [x_ /; x < 0] = 0, f[x_ /; x ≥ 0] := a x^2, f[x_, y_] := x y^b} ]

- Class C2 -

o2 = C2 . new[]

- Object of C2 -

o2 . f[-1]

0

o2 . f[2]

8

o2 . f[3, 4]

3 4^π

Calling a method with an argument list that does not match any definition yields the value Null and triggers an error message.

o2 . f[1, 2, 3]

Class :: nometh : Method f with the specified signature not found.

A method may invoke other methods including itself.

C3 = Class["C3", Class["Element"],  {},  {f[0] = 1, f[n_] := n f[n - 1], g[m_, n_] := m f[n]} ]

- Class C3 -

o3 = C3 . new[]

- Object of C3 -

o3 . f[5]

120

o3 . g[a, 5]

120 a

Note that while the definition f[n_] := n f[n-1]; f[0] = 1 yields in a Mathematica session the expected factorial function, calling it as a class function ends in an infinite loop.

C4 = Class["C4", Class["Element"], {},  {f[n_] := n f[n - 1], f[0] = 1} ]

- Class C4 -

o4 = C4 . new[] ; o4 . f[5]

$RecursionLimit :: reclim : Recursion depth of 256 exceeded.

$RecursionLimit :: reclim : Recursion depth of 256 exceeded.

$RecursionLimit :: reclim : Recursion depth of 256 exceeded.

General :: stop : Further output of $RecursionLimit :: reclim will be suppressed during this calculation.

0

Creating Objects

All classes posses an implicitly defined method new which is used to create objects. This method is available immediately after the class definition.
An object is a structure holding copies of all properties of its class. Upon creation, the properties are initialized to values specified in the class declaration. Unlike classes, objects are allowed to change the values of properties they are storing.

C5 = Class["C5", Class["Element"],  {a = 1, b := Expand[(y + z)^r]},  {f[x_] := x + b} ]

- Class C5 -

o5 = C5 . new[]

- Object of C5 -

The value of the property b stored in o5 is the term (y + z)^r where y, z, and r are global variables of the current Mathematica session.

GetProperties[o5]

{{a, 1}, {b, (y + z)^r}}

The mechanism for creating objects evaluates the initial value for b every time a new object is created.

r = 2 ; o5a = C5 . new[] ; o5a . b

y^2 + 2 y z + z^2

o5a . f[2]

2 + y^2 + 2 y z + z^2

The method new returns a symbol which servers as a reference to the newly created object. This reference is the only way to access the object.

From outside, an object can be accessed by the symbol returned by new. From inside, an object can use the implicit property this to reference itself.

C6 = Class["C6", Class["Element"],  {a = 1, b = 2},  {Prin ... 62371;Print["The other guy's properties are: ", GetProperties[obj]] ]} ]

- Class C6 -

o6a = C6 . new[] ; o6b = C6 . new[] ; o6b . a =  ; o6b . PrintPropertiesOf2[o6a]

My properties are:  {{a, }, {b, 2}}

The other guy's properties are:  {{a, 1}, {b, 2}}

Constructors

Often, it is desirable to have a flexible way of initializing objects. This can be achived by defining constructors. A constructor is a method having the same name as the class itself.

classname[args] := rhs declare a constructor with arguments args

While being declared as any other method, constructors are treated by the class in a special way. The method new may be given some arguments when an object is to be created. After the standard mechanism has created the object, a constructor definition matching the arguments of new is searched for. If one is found, it is invoked with the arguments of new.

Every class has an implicitly defined constructor without parameters - the default constructor.

The class C7 declares two constructors which are used to initialize the property a depending on the type of the argument supplied to new.

C7 = Class["C7", Class["Element"],  {a = 0},  {C7[x_ /; Nu ... tive[x]] := a = x, C7[x_ /; NumericQ[x] && Negative[x]] := a = -∞} ]

- Class C7 -

o7x = C7 . new[] ; o7x . a

0

o7y = C7 . new[1] ; o7y . a

1

o7z = C7 . new[-1] ; o7z . a

-∞

A warning is issued if no matching constructor is found while new has been called with a non empty list of arguments. In that case, the default constructor without arguments is used to construct the new object.

o7u = C7 . new["string"]

Class :: noconstr : No matching constructor found. Using the default constructor.

- Object of C7 -

o7u . a

0

A flexible way of specifying initial values of properties is provided by an implicitly defined constructor that takes a list of rules as its only argument:

class . new[{p_1  v_1, p_2  v_2, ...}] construct a new object and assign each specified property p_i the value v_i ; <br />other properties will be assigned default values

Note that constructors evaluate their arguments. Thus all p_i ' smust evaluate to symbols denoting class' properties. Otherwise a warning is issued and the corresponding property is set to its standard value.

C8 = Class["C8", Class["Element"],  {a = 1, b = 2, c = 3},  {} ]

- Class C8 -

o8 = C8 . new[{a  0, c  -3}] ; GetProperties[o8]

{{a, 0}, {b, 2}, {c, -3}}

Accessing Properties and Methods

Properties and methods are always bound to classes and objects. They cannot be used as stand-alone entities.
The package Elements modifies the standard Dot-operator to access properties and methods from objects and classes.

class . property return the initial value of the specified property as defined in the class declaration
class . f[args] evaluate the class method f with specified arguments
object . property return the current value of the specified property of the object
object . f[args] evaluate the method f with specified arguments

Note that only a few methods may be invoked with a class as the first argument to Dot (like new[]). The majority of methods will depend on properties and require an object to be evaluated.

To set a property of an object, an intuitive notation is provided by Elements:

object . property = rhs set the specified property of the object ; rhs is evaluated immediately
object . property := rhs set the specified property of the object ;   rhs is evaluated each time the value of the property is requested

Get/SetProperties

The Dot-notation makes setting and obtaining the value of a single property easy. When it is desirable to access multiple properties at the same time, the methods GetProperties and SetProperties can be used.

GetProperties[class or object, property] get the value of a class or object property
GetProperties[class or object] get a list of pairs {property, value} for class ' or object ' s properties
GetProperties[class or object, list] get a list of pairs {property, value} for each property in list
GetProperties[class or object, list, h] get a list of pairs {property, value} for each property in list and set the head of each pair to h

An easy way to set properties of objects provides the method SetProperties.

SetProperties[object, property, value] set object ' s property to the specified value
SetProperties[object, {p_1v_1, p_2v_2, …}] set object ' s properties p_i to the values v_i

The methods GetProperties and SetProperties do not evaluate their arguments.
• Class properties are fixed at the time of declaration and cannot be changed by SetProperties.

GetProperties[C8]

{{a, 1}, {b, 2}, {c, 3}}

GetProperties[o8]

{{a, 0}, {b, 2}, {c, -3}}

GetProperties[o8, {a, c}, h]

{h[a, 0], h[c, -3]}

SetProperties[o8, {a10, c30}]

SetProperties[o8, b, 20]

20

GetProperties[o8]

{{a, 10}, {b, 20}, {c, 30}}

Derived Classes and Inheritance

An important paradigm implemented in the package Elements is that of derived classes and inheritance. Every class must have a base class from which it inherits all properties and methods. It can then overide the old or add new properties and methods. If a derived class declares a property or a method which already exists, the old property or method will be shadowed by the new declaration.

The class C9 serves as a base class for C10 which overrides the property a and adds a new definition for the method f with two arguments.

C9 = Class["C9", Class["Element"],  {a = 1, b = 2},  {f[x_] := x^2, g[x_] := a f[x]} ]

- Class C9 -

C10 = Class["C10", C9,  {a = π},  {f[x_, y_] := a x y} ]

- Class C10 -

A new object of C9 will be initialized according to the declarations given in C9, while the property a of an object of C10 will get the initial value π.

o9 = C9 . new[] ; GetProperties[o9]

{{a, 1}, {b, 2}}

o10 = C10 . new[] ; GetProperties[o10]

{{a, π}, {b, 2}}

The value of a in the body of g refers to the current value stored in the object that g was invoked with.

o9 . g[3]

9

o10 . g[3]

9 π

An object of the class C10 can refer to the shadowed value of a from the class C9 by C9.a.

Methods are inherited in a similar way as properties. If a derived class declares a method with the same name and signature as one of its ancestor classes, the old declaration will be shadowed by the new one. If the name is the same but the signature differs, a new definition is associated with that name.

The class C10 contains a declaration of the method f with two arguments. This definition is associated with the symbol f that was introduced in C9. Hence, objects of the class C10 may call f with one or two arguments while for objects of C9 the method f with only one argument is known.

o10 . f[3]

9

o10 . f[3, 4]

12 π

o9 . f[3, 4]

Class :: nometh : Method f with the specified signature not found.

How Methods Are Called

Methods are defined in a way that is very similar to that for ordinary functions in Mathematica. However, the inheritance adds a new level to the evaluation mechanism of Mathematica. The value o.f[args] of calling the method f of the object o of class C with arguments args is determined according to the following scheme:

1. All definitions for f given in the declaration of C are searched. If a matching definition is found, it is used to determine the value.
2. If in 1. no matching definition was found, all definitions in the base class of C are recursively searched.
3. If the search in 1. and 2. was not successful, the value of o.f[args] is Null and a warning is printed that no matching definition was found.

The same procedure is used to search for definitions of higher level calls, i.e. calls initiated from from methods.

The class C11 overrides the definition of f given in C9. The call o11.g[2] uses the definition of g from C9. But, the value of f invoked by g is determined according to the definition from C11 (and not from C9). The reason is that the search for matching definitions is always started in the class of the object that initiated the evaluation.

C11 = Class["C11", C9,  {},  {f[x_] := x^3} ]

- Class C11 -

o11 = C11 . new[]

- Object of C11 -

o11 . f[2]

8

o11 . g[2]

8

The call o9.g[2] uses the same definition for g but it takes the definition of f from C9 to evaluate the body of g.

o9 . g[2]

4

The above approach is used only for methods, i.e. functions defined within classes. It does not apply to global functions. The following example elaborates on this issue.

The function h in the body of f references the global function h in the current Mathematica session.

C12 = Class["C12", Class["Element"],  {a = 2},  {f[x_] := a h[x]} ]

- Class C12 -

o12 = C12 . new[]

- Object of C12 -

o12 . f[1]

2 h[1]

If now a derived class provides an own definition of h, this fact does not influence the definition of f already done in C12. The reason is that at the time f was defined in C12, h did not reference a method but a global function.

C13 = Class["C13", C12,  {},  {h[x_] := Sin[x]} ]

- Class C13 -

o13 = C13 . new[]

- Object of C13 -

o13 . f[1]

2 h[1]

o13 . h[1]

Sin[1]

If a method that is supposed to be overridden later needs to be used in a definition of another method, a "placeholder" method with the correct signature and with an empty body (or any other dummy defiition) can be specified in the class declaration. This will make this method known to the system and open the possibility to override it at any later time.

If we wanted to override h in C13, but make it available in the body of f from C12, we could do the following:

C12new = Class["C12new", Class["Element"],  {a = 2},  {h[x_] = 0, f[x_] := a h[x]} ]

- Class C12new -

o12new = C12new . new[]

- Object of C12new -

o12new . f[1]

0

C13new = Class["C13new", C12new,  {},  {h[x_] := Sin[x]} ]

- Class C13new -

o13new = C13new . new[]

- Object of C13new -

o13new . f[1]

2 Sin[1]

o13new . h[1]

Sin[1]

Implicitly Defined Methods

The package Elements provides a number of additional methods which were not described above:

Class[name] return a reference to the class name
ClassQ[class] True if class is a reference to a class
ClassName[class] the name (string) of the class
BaseClass[class] the base class of class
Properties[class] list of property names (strings) of class
Methods[class] list of method names (strings) of class
ObjectQ[symbol] True if symbol is a reference to an object
ClassOf[obj] the class of object obj
Properties[obj] list of property names (strings) of object obj

Created by Mathematica  (September 15, 2003)