Conversion from DocBook 3.1 (SGML) to DocBook 4.2 (XML). : Brent Fulgham
Copyright © 1995, 1996, 1998, 1999 Eric
Copyright © 2002, 2003, 2004 The Gwydion Maintainers
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
List of Figures
Table of Contents
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.
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, 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;
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
if statement evaluates to 14, otherwise
it returns 11. Since no other statements follow the
, its return value is used as the return value of the entire
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;
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
(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.
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.
Languages with garbage collection have no
need of a
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.
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.
Table of Contents
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
- + ~ ? /, 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.
Dylan Reference Manual
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
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
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”.
Dylan represents true as
#t and false as
#f. When evaluated in a Boolean context, all values
#f return true. Thus, the number zero
—and other common “false” values—evaluate as
true in Dylan.
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.
Dylan uses all three of the “equals” operators
found in C and Pascal, albeit in a different fashion. The Pascal
:=, 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
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
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
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.
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
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.
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
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.
Dylan supports module-level variables,
which serve roughly the same purpose as C's global variables. Although
let function may only be used within
methods (Dylan-speak for regular functions), the forms
define variable and
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.
Table of Contents
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
end method which could in turn be
end method hello-world. In general,
Dylan permits all the obvious combinations of keywords and labels to
follow an end statement.
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;
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
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
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
swap function in Dylan (except by using
macros). However, the following function works just fine, because it
modifies the internal state of another
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.
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;
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
define method square-list(in :: <list>) => out :: <list> map(method(x) x * x end, in); end;
map function takes each element of
in and applies the anonymous method. It
then builds a new list using the resulting values and returns it.
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 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.
A generic function represents zero or more
similar methods. Every method created by means of
method is automatically contained
within the generic function of the same name. For example, a
programmer could define three methods named
, 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
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
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
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.
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
Table of Contents
The features of Dylan's object system don't map directly onto the
features found in C++. Dylan handles access control using
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
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.
Dylan has a large variety of built-in classes. Several of these
represent primitive data types, such as
<character>. A few represent
actual language-level entities, such as
<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”.
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.
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
make sets both slots using the keyword
Only one of
init-function may be
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++
members, plus several other useful allocation schemes. See
Dylan Reference Manual
for the full specifications.
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 “
”. 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, 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;
tax could be invoked as
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
calls a special function named
function invokes the next most specific method of a generic function;
in this case, the method for
objects. Parameters to the current method get passed along
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
, currently requires the use of
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
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.
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
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
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 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,
could be defined as follows:
define abstract class <vehicle> (
<object>) // ...as before end;
The above modification prevents the creation of direct instances
<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
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
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
#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
Table of Contents
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
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
with these arguments performs three separate tasks:
check-insurance. The most specific method on
inspect-vehicle—the one for the classes
—is invoked first and calls
to invoke the less-specific methods in turn.
For an exact definition of “specific”, see the Dylan Reference Manual .
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
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.
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
module, which contains all of the standard built-in
functions and classes. In turn, the
module exports all three of the vehicle classes, the generic function
tax, several getter functions and a single
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.
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 contain modules. For example, the
library contains the
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
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
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
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;
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.
Table 6.1. Standard Mindy Libraries and Modules
|UNIX-style file support|
|threads and semaphores|
|additional table support|
|basic, unformatted I/O|
|support for linking to C|
|trig and other math|
|“poor man's hash table”|
|search, replace support|
|slices of sequences|
|“diff” for sequences|
|strings to/from integers|
|similar to C's “|
|useful string functions|
|similar to Perl|
|similar to Perl|
|UNIX stream I/O|
|the 3 standard streams|
|General object output|
String-Extensions library, in particlar,
contains a number of useful functions. It provides many of Perl's
popular text-manipulation features.
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
define open class <sample> (
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
For more information on sealing, see the chapter “Controlling Dynamism” in the Dylan Reference Manual .
Table of Contents
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.
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
goto statements or the
longjmp function. To use them,
specify a name in the parentheses following a
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
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
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;