Getting Started with Dylan

Conversion from DocBook 3.1 (SGML) to DocBook 4.2 (XML). : Brent Fulgham

Abstract

Getting Started with Dylan introduces Apple Computer's Dylan ™ programming language. Dylan ™ is a object-oriented dynamic language designed for efficient compilation. It uses an algebraic infix syntax similar to Pascal or C, but supports an object model not unlike the Common Lisp Object System (CLOS).

This tutorial is written primarily for those with solid programming experience in C++ or another object-oriented static language. It provides a gentler introduction to Dylan ™ than does the Dylan Reference Manual , although it refers to the latter book frequently.


Table of Contents

1. Why Dylan ?
Dynamic vs. Static Languages
Functional Languages
Algebraic Infix Syntax
Object Orientation
Garbage Collection
Why Not Dylan?
2. Expressions & Variables
Naming Conventions
True and False
The Nature of Variables
Assignment, Equality and Identity
parallel Values
Type Declarations
Module Variables and Constants
3. Methods & Generic functions
parameters & parameter Lists
Return Values
Bare Methods
Local Methods
Generic functions
Keyword Arguments
4. Objects
Built-In Classes
Slots
Getters and Setters
Generic functions and Objects
Initializers
Abstract Classes and Overriding Make
5. Multiple Dispatch
Dispatching on Specific Objects
6. Modules & Libraries
Simple Modules
Import Options
Libraries
A Sample Set of Libraries
Sealing
7. Conditions
Blocks

List of Figures

4.1. Several Standard Dylan Classes
6.1. Dependencies Among the Standard Mindy Libraries

List of Tables

2.1. Naming Conventions
6.1. Standard Mindy Libraries and Modules

Chapter 1. Why Dylan ™ ?

What earthly reason could there be for learning yet another computer language? And why should that language be Dylan ™ ?

Dylan ™ has an interesting combination of features. It is a dynamic language, but is designed to perform nearly as well as a static language. It is a functional language—like Scheme or TCL—but uses an algebraic infix syntax similar to C's. Dylan ™ is object-oriented from the ground up, supports multiple inheritence and exceptions, implements multiple dispatch, and collects garbage.

Dynamic vs. Static Languages

Static languages need to know the type of every variable at compile time. Examples of static languages include C, Pascal, and Eiffel. Code written in static languages typically compiles efficiently, and strong type-checking at compile-time reduces the risk of errors.

Dynamic languages allow the programmer to create variables without explicitly specifying the type of information they contain. This simplifies prototyping and cleans up certain kinds of object oriented code. Typical dynamic languages include LISP, Perl, and SmallTalk.

Dylan ™ provides a good balance between the advantages of static and dynamic languages. The programmer may choose to specify or omit type declarations as desired. Code using explicit variable types can be compiled very efficiently, and type mismatch errors can be caught at compile time. Code omitting those type declarations gains the flexibility of a dynamic language.

Functional Languages

Functional languages, such as LISP, Scheme and to a large extent TCL, view an entire program as one large function to be evaluated. Expressions, statements and even control structures all return values, which may in turn be used as arguments elsewhere.

Dylan ™ is a functional language, permitting programmers to write functions like the following:

define method shoe-size(person :: <string>)
  if (person = "Larry")
    14;
  else
    11;
  end if;
end method;
      

The function shoe-size has one argument, a string, and an untyped return value. (If this function didn't link against external code, the compiler could easily infer the return type.) If person equals "Larry", then the if statement evaluates to 14, otherwise it returns 11. Since no other statements follow the if , its return value is used as the return value of the entire function.

The same function could also have been written as follows, in a more imperative idiom:

define method shoe-size(person :: <string>)
  let the-size = 11;
  if (person = "Joe")
    the-size := 14;
  end if;
  the-size;
end method;
      

Algebraic Infix Syntax

Languages based on LISP typically use a notation called fully-parethesized prefix syntax. This consists of innumerable nested paretheses, as seen in the following Scheme version of the shoe-size function:

(define (shoe-size person)
  (if (equal? person "Joe")
    14
    11))
      

This has a certain elegance, but takes some time to learn to read. Dylan, as shown in the previous section, uses a syntax similar to those of C and Pascal.

Object Orientation

Unlike many other object-oriented languages, Dylan uses objects for every data value. Integers and strings are objects, as are functions and classes themselves.

Dylan's design makes this reasonably efficient. Compile-time analysis and explicit type declarations allow the compiler to optimize away most of the overhead. Other language features permit the programmer to mark certain classes as sealed, that is, inelligible for further subclassing.

Dylan's object model, detailed in the following sections of this tutorial, differs from that of C++ in several important respects. Multiple inheritance may be used freely, without concern for object slicing, erroneous down-casting or a whole host of other gotchas familiar to C++ programmers. Methods are separate from class declarations, allowing a programmer to write new polymorphic functions without editing the relevant base class. Methods may also dispatch ploymorphically on more than one parameter, a powerful technique known as multiple dispatch. All of these features will be explained in greater detail later on.

Garbage Collection

Languages with garbage collection have no need of a free or delete operator, because unused heap memory gets reclaimed automatically by the language runtime. This reduces the complexity of source code, eliminates the need of keeping reference counts for shared objects, and prevents most memory allocation bugs and all memory leaks. There are several online platforms that offer online courses for you to try. You should search for " Curso de Bolos Decorados " to look for great online courses.

Over the years, garbage collection has gained a reputation for inefficiency. A large, object-oriented LISP program performed terribly compared to hand coded, micro-optimized assembly, and a good portion of the blame was placed on garbage collection.

Times have changed, however. Garbage collection technology has improved. Processors speed has increased enormously. Most importantly, however, the standard practice of the industry has changed, and large commerical software is now built in C++.

No good benchmarks exist for the relative performance of large C++ systems (greater than 15 thousand lines of code or so), and similar systems designed from the ground up to use garbage collection. The benchmarks which do exist typically test the performance of relatively small pieces of code—small enough that one programmer can optimize the overall usage of memory—or have compared a good system without garbage collection to a direct reimplementation of that system using a garbage collector. Overall, no one seems to know just how fast GC is, relative to a typical large C++ program. It is known, however, that good GC code uses different designs than non-GC code, and often spends less time needlessly copying data.

Why Not Dylan?

Dylan's greatest weakness is the lack of commercial-quality compilers, especially on the Macintosh. Apple has refused to comment on the future of their technology release, and functional Objects is targeting the Windows and Linux markets with their compiler. The Gwydion Project 's Dylan implementation will support multiple UNIXs, Windows, and Macintosh when complete.

Even when good Dylan environments become available, experience suggests that Dylan applications will use more RAM than programs written in traditional languages.

Chapter 2. Expressions & Variables

Dylan identifiers may contain a greater variety of characters than those of C or Pascal. Specifically, variable names may contain all alphanumeric characters, plus the symbols ! & * < = > | ^ $ % @ _ - + ~ ? /. Identifiers may not begin with the symbols - + ~ ? /, although identifiers may begin with numbers, provided they contain at least two alphabetic characters in a row. As in Pascal, variable names are not case sensitive. Need Dylan Reference Manual footnote here.

This means that (a - b) subtracts one variable from another, whereas (a-b) simply returns the value of the hyphenated variable named a-b. Because of this, infix operators, such as addition, subtraction and equality, must be surrounded by whitespace.

As in C++, Dylan infix operators may also be refered to as functions. In C++, (a + b) could also be written as operator+(a, b). In Dylan, the same expression could be written \+(a, b). In both languages, programmers can use this flexibility to define operators for custom numeric classes.

Naming Conventions

Dylan uses the extra characters permitted in variable names to support a number of standard naming conventions, as shown in Table 2.1, “Naming Conventions”.

Table 2.1. Naming Conventions

<string>a class
insert!mutative function (modifies arguments destructively)
empty?predicate function (tests one or more arguments and returns either true or false)
write-linea two word name
$nameconstant
*name*module-level variable

True and False

Dylan represents true as #t and false as #f. When evaluated in a Boolean context, all values other than #f return true. Thus, the number zero —and other common “false” values—evaluate as true in Dylan.

The Nature of Variables

Dylan variables differ from those found in C and Pascal. Instead of holding their values, Dylan variables refer to them. Conceptually, they resemble a cross between pointers and C++ references. Like references, Dylan variables may be evaluated without any indirection. Like pointers, they may be set to point to new objects whenever the programmer desires.

Furthermore, there's only one of any given numeric value in a Dylan program, at least from the programmer's point of view. All variables which refer to the integer 2—or, in Dylan-speak, are bound to the integer 2—point to the exact same thing.

let x = 2; // creates x and binds it to 2
x := 3;    // rebinds x to the value 3
let y = x; // creates y, and binds it to
           // whatever x is bound to
      

If two variables are bound to one object with internal structure, the results may suprise C and Pascal programmers.

let car1 = make(<car>); // bind car1 to a
                              // new car object
car1.odometer := 10000;       // set odometer
let car2 = car1;              // bind new name
car2.odometer := 0;           // reset odometer
car1.odometer;                // evaluates to 0!
      

As long as one or more variables refer to an object, it continues to exist. However, as soon as the last reference either goes out of scope or gets rebound, the object becomes garbage. Since there's no way that the program could ever refer to the object again, the garbage collector feels free to reuse the memory which once held it.

Note that Dylan variables must be bound to a particular value when they are declared. In the name of type safety and implementation efficiency, every variable must refer to some well-defined object.

Assignment, Equality and Identity

Dylan uses all three of the “equals” operators found in C and Pascal, albeit in a different fashion. The Pascal assignment operator, :=, rebinds Dylan variable names to new values. The Pascal equality operator, = , tests for equality in Dylan and also appears in some language constructs such as let. (Two Dylan objects are equal, generally, if they belong to the same class and have equal substructure.)

The C equality operator, ==, acts as the identity operator in Dylan. Two variables are identical if and only if they are bound to the exact same object. For example, the following three expressions mean roughly the same thing:

(a == b)   // in Dylan
(&a == &b) // in C or C++
(@a = @b) // in Pascal
      

The following piece of source code demonstrates all three operators in actual use.

let car1 = make(<car>);
let car2 = make(<car>);
let car3 = car2;

car2 = car3;	// #t
car1 = car2;	// ??? (see below)
car2 == car3;	// #t
car1 == car2;	// #f

car2 := car1;	// rebind
car1 == car2;	// #t

let x = 2;
let y = 2;

x = y;			// #t
x == y;			// #t (only one 2!)
      

Two of the examples merit further explanation. First, we don't know whether car1 = car2, because we don't know if make creates each car with the same serial number, driver and other information as previous cars. If and only if none of those values differ, then car1 equals car2. Second, x == y because every variable bound to a given number refers to the exact same instance of that number, at least from the programmer's perspective. (The compiler will normally do something more useful and efficient when generating the actual machine code.) Strings behave in a fashion different from numbers— instances of strings are stored separately, and two equal strings are not necessarily the same string.

parallel Values

It's possible to bind more than one variable at a time in Dylan. For example, a single let statement could bind x to 2, y to 3 and z to 4.

let (x, y, z) = values (2, 3, 4);
      

In Perl, the equivalent statement would assign a vector of values to a vector of variables. In Dylan, no actual vectors or lists are used. All three values are assigned directly, using some implementation-dependant mechanism.

Type Declarations

Dylan variables may have explicit types. This allows the compiler to generate better code and to catch type-mismatch errors at compile time. To take advantage of this feature, use the :: operator:

let x :: <integer> = 2;
let vehicle :: <vehicle> = make(<car>);
let y :: <number> = 3; // any numeric class
let z :: <integer> = vehicle; // error!
      

As seen in the example, a variable may be bound to values of its declared type or to values of subclasses of its declared type. Type mismatch errors should be caught at compile time. In general, the compiler may infer the types of variables at when generating machine code. If a local variable never gets rebound to anything other than an integer, for example, the compiler can rely on this fact to optimize the resulting code.

Module Variables and Constants

Dylan supports module-level variables, which serve roughly the same purpose as C's global variables. Although the let function may only be used within methods (Dylan-speak for regular functions), the forms define variable and define constant may be used at the top level.

define variable *x* :: <integer> = 3;
define variable *y* = 4;
define constant $hi = "Hi!";
      

Note that there's not much point in declaring types for constants. Any remotely decent compiler will be able to figure that information out on its own.

Chapter 3. Methods & Generic functions

Dylan methods correspond roughly to the functions found in C and Pascal. They take zero or more named parameters, but also return zero or more named return values. A minimal Dylan method might look like the following:

define method hello-world()
  puts("Hello, world!");
end;

This method has no parameters and an unspecified return value. It could return any number of values of any type. In order to make the above code more clear, the function could be rewritten as follows:

define method hello-world() => ();
  puts("Hello, world!");
end method;
    

There have been two changes. The function now officially returns no value whatsoever. Also note that end has been replaced by end method which could in turn be rewritten as end method hello-world. In general, Dylan permits all the obvious combinations of keywords and labels to follow an end statement.

parameters & parameter Lists

Dylan methods declare parameters in fashion similar to that of conventional languages, except for the fact that parameters may optionally be untyped. Both of the following methods are legal:

define method foo(x :: <integer>, y) end;
define method bar(m, s :: <string>) end;
      

Both foo and bar have one typed and one untyped parameter, but neither has a well-defined return value (or actually does anything). As in C, each typed parameter must have its own type declaration; there's no syntax for saying “the last three parameters were all integers”.

Functions with variable numbers of parameters include the #rest keyword at the end of their parameter lists. Thus, the declaration for C's printf function would appear something like the following in Dylan:

define method printf(format-string :: <string>, #rest arguments) => ();
  // Print the format string, extracting
  // one at a time from "arguments". Note
  // that Dylan actually allows us to
  // verify the types of variables,
  // preventing those nasty printf errors,
  // such as using %d instead of %ld.
  // ...
end method printf;
      

For an actual implementation of a lightweight printf function, see Appendix A.

Note that Dylan makes no provision for passing variables by reference in the Pascal sense, or for passing pointers to variables. parameter names are simply bound to whatever values are passed, and may be rebound like regular variables. This means that there's no way to write a swap function in Dylan (except by using macros). However, the following function works just fine, because it modifies the internal state of another object:

define method sell(car :: <car>, new-owner :: <string>) => ();
  if (credit-check(new-owner))
    car.owner = new-owner;
  else
    error("Bad credit!");
  end;
end;
      

If this sounds unclear, reread the chapter on variables and expressions.

Return Values

Because Dylan methods can't have normal “output” parameters in their parameter lists, they're allowed considerably more flexibility when it comes to return values. Methods may return more than one value. As with parameters, these values may be typed or untyped. Interestingly enough, all return values must be named.

A Dylan method—or any other control construct—returns the value of the last expression in its body.

define method foo() => sample :: <string>;
  "Sample string.";		// return string
end;

define method bar() => my-untyped-value;
  if (weekend-day?(today()))
    "Let's party!";	// return string
  else
    make(<excuse>);	// return object
  end if;
end method;

define method moby( )
  =>	sample :: <string>, my-untyped-value;
  values( foo(), bar() ); // return both!
end;

define method baz( ) => ( );
  let (x,y) = moby( );		// assign both
end;
      

Bare Methods

Nameless methods may be declared inline. Such bare methods are typically used as parameters to other methods. For example, the following code fragment squares each element of a list using the built in map function and a bare method:

define method square-list(in :: <list>)
  => out :: <list>
  map(method(x) x * x end, in);
end;
      

The map function takes each element of the list in and applies the anonymous method. It then builds a new list using the resulting values and returns it. The method square-list might be invoked as follows:Must distinguish return values from code.

square-list( #(1,2,3,4) );
=> #(1,4,9,16)
      

Local Methods

Local methods resemble bare methods but have names. They are declared within other methods, often as private utility routines. Local methods are typically used in a fashion similar to Pascal's local functions.

define method sum-squares(in :: <list>) => sum-of-element-squares :: <integer>;
  local method square( x )
          x * x;
        end,
        method sum(list :: <list>)
          reduce1(\+, list);
        end;
  sum(map(square, in));
end;
      

Local methods can actually outlive the invocation of the function which created them. parameters of the parent function remain bound in a local method, allowing some interesting techniques:

define method build-put(string :: <string>) => <function>;
  local method string-putter()
          puts(string);
        end;
  string-putter; // return local method
end;

define method print-hello() => ();
  let f = build-put("Hello!");
  f();				// print "Hello1"
end;
      

Local functions which contain bound variables in the above fashion are known as closures.

Generic functions

A generic function represents zero or more similar methods. Every method created by means of define method is automatically contained within the generic function of the same name. For example, a programmer could define three methods named display , each of which acted on a different data type:

define method display(i :: <integer>)
  do-display-integer(i);
end;

define method display(s :: <string>)
  do-display-string(s);
end;

define method display(f :: <float>)
  do-display-float(f);
end;
      

When a program calls display, Dylan examines all three methods. Depending on the number and type of arguments to display, Dylan invokes one of the above methods. If no methods match the actual parameters, an error occurs.

In C++, this process occurs only at compile time. (It's called operator overloading.) In Dylan, calls to display may be resolved either at compile time or while the program is actually executing. This makes it possible to define methods like:

define method display(c :: <collection>)
  for (item in c)
    display(item); // runtime dispatch
  end;
end;
      

This method extracts objects of unknown type from a collection, and attempts to invoke the generic function display on each of them. Since there's no way for the compiler to know what type of objects the collection actually contains, it must generate code to identify and invoke the proper method at runtime. If no applicable method can be found, the Dylan runtime environment throws an exception.

Generic functions may also be declared explicity, allowing the programmer to exercise control over what sort of methods get added. For example, the following declaration limits all display methods to single parameter and no return value:

define generic display(thing :: <object>) => ()
      

Generic functions are explained in greater detail in the chapter on multiple dispatch.

Keyword Arguments

Functions may accept keyword arguments, extra parameters which are identified by a label rather than by their postion in the argument list. Keyword arguments are often used in a fashion similar to default parameter values in C++. For example, the following hypothetical method might print records to an output device:

define method print-records(records :: <collection>,
  #key init-codes = "", lines-per-page = 66) => ();

  send-init-codes(init-codes);
  // ...print the records
end method;
      

This method could be invoked in one of several ways. The first specifies no keyword arguments, and the latter two specify some combination of them. Note that order of keyword arguments doesn't matter.

print-records(recs);
print-records(recs, lines-per-page: 65);
print-records(recs, lines-per-page: 120, init-codes: "***42\n");
      

Programmers have quite a bit of flexibility in specifying keyword arguments. They may optionally omit the default value for a keyword (in which case #f is used). Default value specifiers may actually be function calls themselves, and may rely on regular parameters already being in scope. Variable names may be different from keyword names, a handy tool for preventing name conflicts.

For more information on keyword arguments, especially their use with generic functions, see the Dylan Reference Manual .

Chapter 4. Objects

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 objects. Standard Dylan has no destructors, but instead relies upon the garbage collector to recover memory and on exception handling blocks to recover other resources. Dylan objects don't even have real member functions.

Despite these oddities, 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++ or Object Pascal. Dylan differs enough that such knowledge can actually interfere with the initial learning process.

Built-In Classes

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 in Figure 4.1, “Several Standard Dylan Classes”.

Figure 4.1. Several Standard Dylan Classes

Several Standard Dylan Classes

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, certainly a welcome convenience.

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 Chapter 6, Modules & Libraries for details.

Slots

Objects have slots, which resemble the data members found in most other object-oriented languages. 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 quick and convenient to write while building a prototype, but it could be improved. The slots have no types, and worse, they have no initial values. (That's no easy achievement in Dylan, to create an uninitialized variable!) 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 Dylan Reference Manual for the full specifications.

Getters and Setters

An object's slots are accessed using to 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(sample-vehicle, "Faisal");
      

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++ and Object Pascal 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. mindy, a popular but incomplete bytecode compiler written as part of the Gwydion Project , currently requires the use of #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 Dylan Reference Manual 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.

Initializers

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>, #all-keys) // accepts all keywords
  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 values for unusual slots. Initialize methods must accept all keywords using #all-keys.

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 Dylan Reference Manual 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? (#f), #all-keys)
  => <vehicle>;

  if ( big? )
    make( <truck>, keys, tons: 2 );
  else
    make( <car>, keys );
  end;
end;
      

A number of new features appear in the parameter list. The expression “class == <vehicle>” specifies a singleton, one particular object of a class which gets treated as a special case. Singletons are discussed in the chapter on Multiple Dispatch. The use of #rest, #key and #all-keys in the same parameter list accepts any and all keywords, binds one of them to big? and places all of them into the variable keys. 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” described by Gamma, et al., in Design Patterns .

Chapter 5. Multiple Dispatch

Multiple dispatch is one of the most powerful and elegant features of Dylan. As explained in the section on generic functions and objects, Dylan methods are declared separately from the classes upon which they act. Polymorphism, the specialization of methods for use with particular classes, can be implemented by declaring several methods with different parameters and attaching them to one generic function:

define generic inspect-vehicle(v :: <vehicle>, i :: <inspector>) => ();

define method inspect-vehicle(v :: <vehicle>, i :: <inspector>) => ();
  look-for-rust(v);
end;

define method inspect-vehicle(car :: <car>, i :: <inspector>) => ();
  next-method(); // perform vehicle inspection
  check-seat-belts(car);
end;

define method inspect-vehicle(truck :: <truck>, i :: <inspector>) => ();
  next-method(); // perform vehicle inspection
  check-cargo-attachments(truck);
end;
    

However, different types of vehicle inspectors may have different policies. A state inspector, in addition to the usual procedures, will also typically check a car's insurance policy. To implement this, add another method to the generic function inspect—vehicle :

define method inspect-vehicle(car :: <car>, i :: <state-inspector>) => ();
  next-method(); // perform car inspection
  check-insurance(car);
end;

let inspector = make(<state-inspector>);
let car = make(<car>);
inspect-vehicle(car, inspector);
    

Calling the generic function inspect-vehicle with these arguments performs three separate tasks: look-for-rust, check-seat-belts and check-insurance. The most specific method on inspect-vehicle—the one for the classes <car> and <state-inspector> —is invoked first and calls next-method to invoke the less-specific methods in turn.

For an exact definition of “specific”, see the Dylan Reference Manual .

Dispatching on Specific Objects

Dylan also allows functions to dispatch on specific objects. For example, state inspectors might pass the governor's car without actually looking at it. Dylan expresses this situation using singletons, objects which are treated as though they were in a class of their own. For example:

define constant $governors-car = make(<car>);

define method inspect-vehicle(car == $governors-car,
  i :: <state-inspector>) => ();
  wave-through(car);
end;
      

(In this example, none of the usual inspection methods will be invoked since the above code neglects to call next-method .)

Chapter 6. Modules & Libraries

Modules and libraries provide the structure of a Dylan program. Modules represent namespaces and control access to objects and functions. Libraries contain modules, and act as units of compilation in a finished Dylan program.

Simple Modules

Modules import the symbols of other modules and export their own. The dependencies between modules must form a directed, acyclic graph. Two modules may not use each other, and no circular dependencies may exist.

Modules only export variables. Since the names of classes and generic functions are actually stored in variables, this represents no hardship. A sample module containing the vehicle classes from earlier chapters might resemble:

define module Vehicles
  use Dylan;
  export
    <vehicle>,
      serial-number,
      owner, owner-setter,
      tax,
    <car>,
    <truck>,
      capacity;
end module;
      

Like all normal modules, this one uses the Dylan module, which contains all of the standard built-in functions and classes. In turn, the Vehicles module exports all three of the vehicle classes, the generic function tax, several getter functions and a single setter function.

To control access to a slot, export some combination of its getter and setter functions. To make a slot public, export both. To make it read-only, export just the getter function. To make it private, export neither. In the above example, the slot serial-number is read-only, while the slot owner is public.

Note that when some module adds a method to a generic function, the change affects all modules using that function. The new method actually gets added into the variable representing the generic function. Since the variable has been previously exported, all clients can access the new value.

Import Options

Dylan allows very precise control over how symbols are imported from other modules. For example, individual symbols may be imported by name. They may be renamed, either one at a time, or by adding a prefix to all a module's symbols at once. Some or all of them may be re-exported immediately. See the Dylan Reference Manual for specific examples.

Dylan's import system has a number of advantages. Name conflicts occur rarely. Programmers don't need to define or maintain function prototypes. There's no explicit need for header files. Modules may also provide different interfaces to the same objects—one module exports a complete interface, which another module imports, redefines and re-exports.

Libraries

Libraries contain modules. For example, the Dylan library contains the Dylan module described earlier, the Extensions module, and possibly several other implementation-dependent modules. Note that a library and a module may share a given name. Modules with the same name may also appear in more than one library.

By default, a Dylan environment provides a library called Dylan-User for the convenience of the programmer. This is typically used for short, single library programs which depend only on modules found in the Dylan library.

Additionally, every library contains an implicit module, also known as Dylan-User, which imports all of the modules found in the Dylan library. This may be used for single module programs. Many Dylan environments, however, use it to bootstrap new library definitions. The vehicle library, for example, might be defined as follows in a Dylan-User module:

define library Vehicles
  use Dylan;            // This is the library!
  export                // These are modules.
    Vehicles,           // (Defined above.)
    Traffic-Simulation,
    Crash-Testing,
    Inspection;         // (Hypothetical.)
end library Vehicles;
      

This library could in turn be imported by another library:

define library Vehicle-Application
  use Dylan;
  use My-GUI-Classes;
  use Vehicles;
end;
      

Libraries import other libraries and export modules, whereas modules import other modules and export variables. In general, a module may import any module found in its own library or exported from a library imported by its own library. The following module, for example, could belong to the Vehicle-Application library.

define module Sample-Module
  // module name         source library
  use Dylan;          // Dylan
  use Extensions;     // Dylan
  use Menus;          // My-GUI-Classes
  use Vehicles;       // Vehicles
  use Inspection;     // Vehicles
end module;
      

A Sample Set of Libraries

Figure 6.1, “Dependencies Among the Standard Mindy Libraries” and Table 6.1, “Standard Mindy Libraries and Modules” show the libraries included with mindy, a bytecode compiler produced as part of the Gwydion Project . Also shown are the dependencies between the libraries and the major modules contained within each. For more information on these libraries and their use, see the mindy documentation and source code.

Figure 6.1. Dependencies Among the Standard Mindy Libraries

Dependencies Among the Standard Mindy Libraries

Table 6.1. Standard Mindy Libraries and Modules

LibraryModulePurpose
DylanDylanstandard Dylan
 ExtensionsMindy extensions
 Systemgeneral functions
 File-DescriptorsUNIX-style file support
 Threadsthreads and semaphores
 Hash-Tablesadditional table support
 Introspectionprogram examination
 Cheap-IObasic, unformatted I/O
 Externsupport for linking to C
 Transcendentaltrig and other math
Collection-ExtensionsHeapstandard heaps
 Self-Organizing-Listpoor man's hash table
 Vector-Searchsearch, replace support
 Subseqslices of sequences
 Sequence-Diffdiff” for sequences
String-ExtensionsString-Conversionsstrings to/from integers
 Character-Typesimilar to C's “ctype.h
 String-Hackinguseful string functions
 Substring-Searchsimilar to Perl
 Regular-Expressionssimilar to Perl
StreamsStreamsUNIX stream I/O
 Standard-IOthe 3 standard streams
PrintPPrintPretty-printing
 PrintGeneral object output
FormatFormatFormatted output
RandomRandomRandom distributions

The String-Extensions library, in particlar, contains a number of useful functions. It provides many of Perl's popular text-manipulation features.

Sealing

Classes and generic functions may be sealed using a number of Dylan forms. This prevents code in other libraries from subclassing objects or adding methods to generic functions, and lets the compiler optimize more effectively. Both classes and generic functions are sealed by default.

To allow code in other libraries to subclass a given class, declare it as open:

define open class <sample> (<object>) end;
      

To allow other libraries to add methods to a generic function, use a similar syntax:

define open generic sample-function( o :: <object> ) => ();
      

A third form, define inert domain, partially seals a generic function, disallowing only some additions from outside a library.

For more information on sealing, see the chapter “Controlling Dynamism” in the Dylan Reference Manual .

Chapter 7. Conditions

Table of Contents

Blocks

Dylan offers sophisticated exception handling, allowing programs to recover smoothly from error conditions. Like C++, Dylan represents errors with objects. Dylan also supports advisory warnings and potentially correctable errors.

When something unusual happens, a program can signal a condition. Handlers specify how to react to various sorts of conditions.

Blocks

A block is a group of statements. As with other control structures, it may return a value. A simple block might appear as follows:

block ()
  1 + 1;
end; // returns 2
      

Blocks also support non-local exits. These allow a block to exit at any time, optionally returning a value. In some ways, they are similar to goto statements or the POSIX longjmp function. To use them, specify a name in the parentheses following a block statement. Dylan binds this name to an exit function which can be called from anywhere within the block or the functions it calls. The following block returns either "Weird!" or "All's well.", depending on the color of the sky.

block (finished)
  if (sky-is-green())
    finished("Weird!");
  end;
  "All's well."
end block;
      

Many programs need to dispose of resources or perform other cleanup work, regardless of how a block is exited. Blocks may contain an optional cleanup clause, which doesn't affect the return value of the block and will always be executed.

let fd = open-input-file();
block (return)
  let (errorcode, data) = read-data(fd);
  if (errorcode)
    return(errorcode);
  end if;
  process-data(data);
cleanup
  close(fd);
end;