The features of Dylan’s object system don’t map directly onto the features found in C++. Dylan handles access control using modules, not private declarations within individual classes. Standard Dylan has no destructors, but instead relies upon the garbage collector to recover memory and on block/cleanup to recover lexically scoped resources. Dylan objects don’t even have real member functions.
Dylan’s object system is at least as powerful as that of C++. Multiple inheritance works smoothly, constructors are rarely needed and there’s no such thing as object slicing. Alternate constructs replace the missing C++ features. Quick and dirty classes can be turned into clean classes with little editing of existing code.
Before starting, temporarily set aside any low-level expertise in C++. Dylan differs enough that such knowledge can actually interfere with the initial learning process.
Dylan has a large variety of built-in classes. Several of these represent primitive data types, such as <integer> and <character>. A few represent actual language-level entities, such as <class> and <function>. Most of the others implement collection classes, similar to those found in C++’s Standard Template Library. A few of the most important classes are shown here:
The built-in collection classes include a number of common data structures. Arrays, tables, vectors, ranges and deques should be provided by all Dylan implementations. The language specification also standardizes strings and byte-strings.
Not all the built-in classes may be subclassed. This allows the compiler to heavily optimize code dealing with basic numeric types and certain common collections. The programmer may also mark classes as sealed, restricting how and where they may be subclassed. See Sealing for details.
Objects have slots, which resemble the data members in C++ or fields in Java. Like variables, slots are bound to values; they don’t actually contain their data. A simple Dylan class shows how slots are declared:
define class <vehicle> (<object>) slot serial-number; slot owner; end;
The above code would be quick and convenient to write while building a prototype, but it could be improved. The slots have no declared types so they default to <object>, and they don’t specify default values so they default to #f. The following snippet fixes both problems:
define class <vehicle> (<object>) slot serial-number :: <integer>, required-init-keyword: sn:; slot owner :: <string>, init-keyword: owner:, // optional init-value: "Northern Motors"; end class <vehicle>;
The type declarations work just like type declarations anywhere else in Dylan; they limit a binding to objects of a given class or of one of its subclasses, and they let the compiler optimize. The new keywords describe how the slots get their initial values. (The keyword init-function may also be used; it must be followed by a function with no arguments and the appropriate return type.)
To create a vehicle object using the new class declaration, a programmer could write one of the following:
make(<vehicle>, sn: 1000000) make(<vehicle>, sn: 2000000, owner: "Sal")
In the first example, make returns a vehicle with the specified serial number and the default owner. In the second example, make sets both slots using the keyword arguments.
Only one of required-init-keyword, init-value and init-function may be specified. However, init-keyword may be paired with either of the latter two if desired. More than one slot may be initialized by a given keyword.
Dylan also provides for the equivalent of C++ static members, plus several other useful allocation schemes. See the DRM for the full details.
Getters and Setters¶
An object’s slots are accessed using two functions: a getter and a setter. By default, the getter function has the same name as the slot, and the setter function appends “-setter”. These functions may be invoked as follows:
owner(sample-vehicle); // returns owner owner-setter("Faisal", sample-vehicle);
Dylan also provides some convenient “syntactic sugar” for these two functions. They may also be written as:
sample-vehicle.owner; // returns owner sample-vehicle.owner := "Faisal";
Generic functions and Objects¶
Generic functions, introduced in Methods and Generic functions, provide the equivalent of C++ member functions. In the simplest case, just declare a generic function which dispatches on the first parameter.
define generic tax (v :: <vehicle>) => (tax-in-dollars :: <float>); define method tax (v :: <vehicle>) => (tax-in-dollars :: <float>) 100.00 end; //=== Two new subclasses of vehicle define class <car> (<vehicle>) end; define class <truck> (<vehicle>) slot capacity, required-init-keyword: tons:; end; //=== Two new "tax" methods define method tax (c :: <car> ) => (tax-in-dollars :: <float>) 50.00 end method; define method tax (t :: <truck>) => (tax-in-dollars :: <float>) // standard vehicle tax plus $10/ton next-method() + t.capacity * 10.00 end method;
The function tax could be invoked as tax(v) or v.tax, because it only has one argument. Generic functions with two or more arguments must be invoked in the usual Dylan fashion; no syntactic sugar exists to make them look like C++ member functions.
The version of tax for <truck> objects calls a special function named next-method. This function invokes the next most specific method of a generic function; in this case, the method for <vehicle> objects. Parameters to the current method get passed along automatically.
Technically, next-method is a special parameter to a method, and may be passed explicitly using #next.
define method tax (t :: <truck>, #next next-method) => (tax-in-dollars :: <float>) // standard vehicle tax plus $10/ton next-method() + t.capacity * 10.00 end method;
Dylan’s separation of classes and generic functions provides some interesting design ideas. Classes no longer need to “contain” their member functions; it’s possible to write a new generic function without touching the class definition. For example, a module handling traffic simulations and one handling municipal taxes could each have many generic functions involving vehicles, but both could use the same vehicle class.
Slots in Dylan may also be replaced by programmer-defined accessor functions, all without modifying existing clients of the class. The DRM describes numerous ways to accomplish the change; several should be apparent from the preceding discussion. This flexibility frees programmers from creating functions like GetOwnerName and SetOwnerName, not to mention the corresponding private member variables and constructor code.
For even more creative uses of generic functions and the Dylan object model, see the chapter on Multiple Dispatch.
The make function handles much of the drudgery of object construction. It processes keywords and initializes slots. Programmers may, however, customize this process by adding methods to the generic function initialize. For example, if vehicle serial numbers must be at least seven digits:
define method initialize (v :: <vehicle>, #key) next-method(); if (v.serial-number < 1000000) error("Bad serial number!"); end if; end method;
initialize methods get called after regular slot initialization. They typically perform error checking or calculate derived slot values. Initialize methods must specify #key in their parameter lists.
It’s possible to access the values of slot keywords from initialize methods, and even to specify additional keywords in the class declaration. See the DRM for further details.
Abstract Classes and Overriding Make¶
Abstract classes define the interface, not the implementation, of an object. There are no direct instances of an abstract class. Concrete classes actually implement their interfaces. Every abstract class will typically have one or more concrete subclasses. For example, if plain vanilla vehicles shouldn’t exist, <vehicle> could be defined as follows:
define abstract class <vehicle> (<object>) // ...as before end;
The above modification prevents the creation of direct instances of <vehicle>. At the moment, calling make on this class would result in an error. However, a programmer could add a method to make which allowed the intelligent creation of vehicles based on some criteria, thus making <vehicle> an instantiable abstract class:
define method make (class == <vehicle>, #rest keys, #key big?) => (vehicle :: <vehicle>) if (big?) make(<truck>, keys, tons: 2) else make(<car>, keys) end end method make;
A number of new features appear in the parameter list. The expression “class == <vehicle>” specifies a singleton dispatch, meaning this method will be called only if class is exactly <vehicle>, not a subclass such as <car>. Singleton dispatch is discussed in the chapter on Multiple Dispatch. The use of #rest and #key in the same parameter list means all keyword arguments will be stored in the keys parameter but if big? is passed it will be bound to the variable by the same name. The new make method could be invoked in any of the following fashions:
let x = 1000000; make(<vehicle>, sn: x, big?: #f); //=> car make(<vehicle>, sn: x, big?: #t); //=> truck make(<vehicle>, sn: x); //=> car
Methods added to make don’t actually need to create new objects. Dylan officially allows them to return existing objects. This can be used to manage lightweight shared objects, such as the “flyweights” or “singletons” described by Gamma, et al., in Design Patterns.