Dylan Programming
Dylan Programming
Dylan Programming
Release 1.0
CONTENTS
1 2
Front Matter Preface 2.1 Dylan . . . . . . . . . . . . . 2.2 Audience . . . . . . . . . . . 2.3 Goals of this book . . . . . . 2.4 Organization of this book . . 2.5 Program examples . . . . . . 2.6 Conventions used in this book 2.7 An image of Dylan . . . . . . 2.8 Acknowledgments . . . . . .
3 5 5 5 5 6 6 6 7 7 9 9 14 24 31 39 53 57 67
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
Part 1. Basic Concepts 3.1 Introduction . . . . . . . . . . . . 3.2 Quick Start . . . . . . . . . . . . . 3.3 Methods, Classes, and Objects . . . 3.4 User-Dened Classes and Methods 3.5 Class Inheritance . . . . . . . . . . 3.6 Multimethods . . . . . . . . . . . . 3.7 Modularity . . . . . . . . . . . . . 3.8 A Simple Library . . . . . . . . . . Part 2. Intermediate Topics 4.1 Nonclass Types . . . . . . . . 4.2 Slots . . . . . . . . . . . . . 4.3 Collections and Control Flow 4.4 Functions . . . . . . . . . . . 4.5 Libraries and Modules . . . . 4.6 Four Complete Libraries . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
75 . 75 . 80 . 92 . 108 . 123 . 144 155 155 160 165 187 187 202 219 231 i
Part 3. Sample Application 5.1 Design of the Airport Application . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.2 Denition of a New Collection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.3 The Airport Application . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Part 4. Advanced Topics 6.1 Multiple Inheritance . . . . 6.2 Performance and Flexibility 6.3 Exceptions . . . . . . . . . 6.4 Macros . . . . . . . . . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
7 8
Source Code of Program Examples Resources on Dylan 8.1 World Wide Web pages for this book and its examples 8.2 Newsgroup . . . . . . . . . . . . . . . . . . . . . . . 8.3 Harlequin . . . . . . . . . . . . . . . . . . . . . . . . 8.4 Carnegie Mellon University . . . . . . . . . . . . . . 8.5 Apple Computer, Inc. . . . . . . . . . . . . . . . . . 8.6 Digitool, Inc. . . . . . . . . . . . . . . . . . . . . . . 8.7 Marlais . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
Dylan Object Model for C and C++ Programmers 247 9.1 The concept of pointers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 247 9.2 The concept of classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 250 255 261
10 Glossary Index
ii
This book will be useful to anyone learning dynamic, object-oriented programming, whether it be in Dylan, Java, Smalltalk, or Lisp. Andrew Shalit, author of The Dylan Reference Manual.
CONTENTS
CONTENTS
CHAPTER
ONE
FRONT MATTER
by Neal Feinberg, Sonya E. Keene, Robert O. Mathews, P. Tucker Withington Harlequin Incorporated Published by Addison-Wesley Longman, ISBN 0-201-47976-1 The authors have all worked on Harlequins Dylan product. Neal Feinberg manages the development of Harlequins database technology. Sonya E. Keene, author of Object-Oriented Programming in Common Lisp (Addison Wesley Longman, 1989) is also involved in publishing large documents ont the World Wide Web. Robert O. Mathews was previously the OSF documentation project leader for Motif and DCE. P. Tucker Withington designs and develops automatic memory-management facilities for Dylan. Dylan Programming is available in print from Harlequin, Addison-Wesley, or your bookseller. Copyright 1994, 1995, 1996 by The Harlequin Group Limited. All rights reserved.
CHAPTER
TWO
PREFACE
2.1 Dylan
Dylan (DYnamic LANguage) is a new programming language invented by Apple Computer and several partners. Dylan is dynamic, is object-oriented, and delivers efcient applications. The Dylan language is dened by The Dylan Reference Manual, written by Andrew Shalit, and published by AddisonWesley (1996). That manual is the denitive reference on Dylan. The Dylan Reference Manual is available on the World Wide Web; see Resources on Dylan, for details. Dylan is up and running. You can get it from Harlequin, Carnegie Mellon University, Apple Computer, Digitool, and other organizations. Dylan implementations run on most of the popular computer platforms. Full-edged implementations provide both a compiler and a development environment. You can obtain public-domain implementations. See Resources on Dylan.
2.2 Audience
This book is written for application programmers who have experience working in a conventional language, such as C, Pascal, COBOL, FORTRAN, or BASIC, or in an object-oriented language, such as C++, Java, Smalltalk, or Common LISP with CLOS. Familiarity with object-oriented programming and dynamic languages is not required. We do compare Dylan to C, C++, and Java in this book, but you can read and understand the book without any knowledge of C, C++, or Java.
Introduces the more advanced features of Dylan, including multiple inheritance, performance, exceptions, and macros. This book does not attempt to be as complete as The Dylan Reference Manual, and does not provide the following kinds of material: Complete descriptions of all classes and functions provided by Dylan Complete descriptions of the detailed mechanisms in Dylan To make full use of Dylan, programmers need The Dylan Reference Manual, as well as this book.
Chapter 2. Preface
// Method that says a greeting define method say-greeting (greeting :: <object>); format-out("%s\n", greeting); end;
Many Dylan environments provide a listener, which enables you to type in expressions and to see their return values and output. We use a hypothetical Dylan listener to show the result of evaluating Dylan expressions:
? say-greeting("hi, there"); => hi, there In our hypothetical listener, the Dylan prompt is the question mark, ?. The *bold typewriter font* shows what the user types. The *bold-oblique typewriter font* shows what the listener displays.
We use boxes to give information about Dylans naming conventions, cautions, performance implications, comparisons to other languages such as C or C++, environment notes, and automatic-storage-management notes. Here is an example: Environment note: Our hypothetical development environment does not represent any particular Dylan development environment. Also note that the Dylan language does not require a development environment, so any given implementation may not provide one.
2.8 Acknowledgments
We are fortunate to have at Harlequin a great pool of Dylan talent and expertise, including original inventors of the language, compiler gurus, and environment designers. A core group of Dylan experts and two expert C programmers gave us valuable technical advice and encouragement from the rst to the nal days of our project: Freeland Abbott, Jonathan Bachrach, Kim Barrett, Paul Butcher, Paul Haahr, Tony Mann, and Keith Playford. Other people reviewed our drafts along the way: Roman Budzianowski, Bob Cassels, Edward Cessna, Bill Chiles, Christopher Fry, David Gray, Eliot Miranda, Scott McKay, Nosa Omorogbe, Mike Plusch, and Andy Sizer. We are grateful to Harlequin people whose expertise lies in programming languages other than Dylan, for giving us their perspectives on our book: Judy Anderson, Wesley Dunnington, David Jones, Andy Latto, Peter Norvig, Kent Pitman, Steve Rowley, Craig Swanson, Jason Trenouth, Helen Vickers, and Evan Williams. Andrew Shires carefully tested all our program examples. Brent Tennefoss gave us a great deal of help with graphics. Gary Palter shared his Macintosh expertise, and Leah Bateman shared her Windows expertise. Richard Brooksby let 2.7. An image of Dylan 7
us steal time from other projects to write this book. Anne Altherr, Sharon Van Gundy, Clive Harris, and Sang Lee helped us to navigate the legal and business issues. Ken Jackson helped us to get the ball rolling, and gave it an extra push when needed. Jo Marks is one of Dylans biggest fans he urged us to write this book as a way to explain the power of Dylan to a wider audience. We are grateful to Dylan experts outside of Harlequin who gave us thoughtful and thorough reviews of the book: Scott Fahlman, Robert Futrelle, David Moon, and Andrew Shalit. Our editors at Addison-Wesley cheerfully and capably steered us through the process and helped to shape our book. We are grateful to Sarah Hallet Corey, Lyn Dupr, Nancy Fenton, and Helen Goldstein. Eileen Hoff designed the cover using Bachrachs image. It was, once again, a great pleasure to work with Peter Gordon. We thank the people at Apple Computer who combined their vision of the future with hard work to make Dylan a reality. We thank the people at Carnegie Mellon University and Harlequin who continue to move Dylan forward with insight and creativity.
Chapter 2. Preface
CHAPTER
THREE
A dynamic language allows you to make more run-time changes to program structure, such as passing arguments of different types to the same function and, in some languages, dening new types or classes. A dynamic environment might allow run-time denition and linking. Languages near the dynamic end of the axis include Common LISP and Smalltalk. In reality, few languages in commercial use are purely procedural or object oriented, purely static or dynamic. In fact, the trend has been to add missing elements from one pole to languages that are close to the opposite pole. C++ adds object-oriented features to C; dynamic linking is becoming more common; LISP and Smalltalk vendors have made applications smaller and more efcient. This work, however, is hampered by the need to maintain compatibility with features of the language that were not designed with objects, dynamism, or performance in mind.
Figure 3.1: Object-oriented and dynamic extents of programming languages. Dylan, in contrast, is a new language that integrates the best ideas from object-oriented, procedural, dynamic, and static languages, while avoiding many of the drawbacks. Object-oriented and dynamic extents of Dylan and other languages. shows where Dylan ts on the graph. Dylans goals are simple: Promote modular, reusable, component-oriented programs.
10
Figure 3.2: Object-oriented and dynamic extents of Dylan and other languages.
3.1. Introduction
11
Support powerful and familiar procedural programming. Encourage rapid and productive development of programs. Permit delivery of safe, efcient, compact applications. Lets take a brief look at features of Dylan that support these goals.
12
Dylan has a rich set of variable-sized aggregate data types, called collections. Collection classes include strings, arrays, sets, queues, lists, stacks, and tables. Dylan has exible iteration constructs and permits applications to extend them so that they operate on application-dened collection subclasses. In this way, a module that uses specialized collection classes can cooperate with another module that denes general collection operations. Dylan has a built-in exception-signaling and exception-handling system that permits both error handling and recovery. Exceptions are based on a class and object model that ts smoothly with the rest of the language and can be extended by the program. You do not have to return and check error codes from functions an error-prone process in itself to ensure that no exception has occurred.
3.1. Introduction
13
so that an application that specializes the interface does not have to be recompiled to use a new version of the library. Most Dylan implementations provide support for operating in a multilanguage environment. A Dylan program can operate with code written in another language, and a program written in another language can operate with Dylan code. You can use a Dylan program as a component of a software system that includes code written in other languages. Dylans overall aim is to meet two needs that have often been in conict: 1. To give programmers the freedom and power to develop applications rapidly 2. To deliver components and applications that can run efciently on a wide range of machines and operating systems This book introduces you to the features of Dylan that make those goals attainable. We think you will nd Dylan to be a language that makes your programming time both productive and enjoyable.
In our hypothetical listener, the Dylan prompt is the question mark, ?. The user types in 7 + 12; and presses Enter. The listener executes the expression and displays the value returned by that expression, which is 19. The listener displays any return values and output produced by the expression. Environment note: Our hypothetical development environment does not represent any particular Dylan development environment. The Dylan language does not require a development environment, so any given implementation may not provide one.
14
Caution: Spaces are needed! In Dylan, it is legal to use characters such as +, -, *, <, >, and / in names of variables. Therefore, in most cases, you must leave spaces around those characters in code, to make it clear that you are using them as functions, and that they are not part of the name of a variable. For example: a + b means add a and b. a+b means the name a+b. We can multiply several numbers together:
? 24 * 7 * 52; => 8736
The functions =, <, and > are predicates. A predicate returns true if the condition it is testing is true; otherwise, it returns false. As you might guess, #t means true and #f means false. False is represented by the unique value #f only, but any object that is not #f is true (thus, 0 is a true value). Comparison with C and C++: Caution! C and C++ use integers to represent Boolean values 0 represents false, and any nonzero value is considered true. Dylan has an explicit <boolean> type with two instances: #f represents false, and #t represents the canonical true value. However, any value other than #f is also considered true in a Boolean test. Thus, in Dylan, 0 is considered true.
Comparison with Java: Java has a separate type for Boolean values. Unlike Dylan, C, or C++, the Java Boolean class has only two values, true and false. This design allows the compiler to issue warnings for the common C error if (a=b) ..., because an assignment does not typically yield a Boolean result. An explicit conversion is required to test nonzero in Java: if (a!=0) ....
15
Inx syntax and function-call syntax The functions +, -, *, <, >, and = use inx syntax; that is, the function name appears between the arguments to the function. Most other Dylan functions use the function-call syntax shown in the following call to the min function, which returns the smallest of its arguments:
? min(2, 4, 6); => 2
The function name appears rst, followed by its arguments, which are surrounded by parentheses and separated by commas. Other examples of the function-call syntax follow:
? even?(3); => #f ? zero?(0); => #t
Convention: The names of most predicates end with a question mark for example, even?, odd?, zero?, positive? and negative?. The question mark is part of the name, and does not have any special behavior. There are exceptions to this convention, such as the predicates named =, <, and >.
Case insensitivity Dylan is case insensitive. Therefore, we can call the max function as follows:
? MAX(-1, 1); => 1 ? mAx(0, 55.3, 92); => 92
In Dylan, these variables are called module variables. A module variable has a name and a value. For now, you can consider module variables to be like global variables in other languages. (See Modules, for information about modules.) Module variables can have different values assigned to them during the execution of a program. When you dene a module variable, you must initialize it; that is, you must provide an initial value for it. For example, the initial value of *my-number* is 7. Convention: Module variables have names that start and end with an asterisk for example, *my-number*. The asterisks are part of the name, and do not have any special behavior.
16
We can use the assignment operator, :=, to change the values stored in a variable:
? *my-number* := 100; => 100
Assignment, initialization, and equality People new to Dylan may nd = and := confusing, because the names are similar, and the meanings are related but distinct. The meaning of = depends on whether it appears an expression, or in a denition of a variable or constant. In an expression, = is a function that tests for equality; for example,
? 3 = 3; => #t
In a denition of a variable or constant, = precedes the initial value of the variable or constant; for example,
? define variable *her-number* = 3;
The assignment operator, :=, performs assignment, which is setting the value of an existing variable; for example,
? *her-number* := 4; => 4
After you have assigned a value to a variable, the = function returns true:
? *her-number* = 4; => #t
Dylan offers an identity predicate, which we discuss in Predicates for testing equality. Variables that have type constraints We dened the variables *my-number* and *your-number* without giving a type constraint on the variables. Thus, we can store any type of value in these variables. For example, here we use the assignment operator, :=, to store strings in these variables:
17
What happens if we try to add the string values stored in these variables?
? *my-number* + *your-number*; => ERROR: No applicable method for + with arguments ("seven", "twelve")
Dylan signals an error because the + function does not know how to operate on string arguments. Environment note: The Dylan implementation denes the exact wording of error messages, and what happens when an error is signaled. If your implementation opens a Dylan debugger when an error is signaled, you now have an opportunity to experiment with the debugger! We can redene the variables to include a type constraint, which ensures that the variables can hold only numbers. We specify that *my-number* can hold any integer, and that *your-number* can hold a single-precision oatingpoint number: ? dene variable my-number :: <integer> = 7; ? dene variable your-number :: <single-oat> = 12.01; What happens if we try to store a string in one of the variables?
? *my-number* := "seven"; => ERROR: The value assigned to *my-number* must be of type <integer>
Both <integer> and <single-float> are classes. For now, you can think of a class as being like a datatype in another language. Dylan provides a set of built-in classes, and you can also dene new classes. Convention: Class names start with an open angle bracket and end with a close angle bracket for example, <integer>. The angle brackets are part of the name, and do not have any special behavior. The + function can operate on numbers of different types:
? *my-number* + *your-number*; => 19.01
Module constants A module constant is much like a module variable, except that it is an error to assign a different value to a constant. Although you cannot assign a different value to a constant, you may be able to change the elements of the value, such as assigning a different value to an element of an array. You use define constant to dene a module constant, in the same way that you use define variable to dene a variable. You must initialize the value of the constant, and you cannot change that value throughout the execution of a Dylan program. Here is an example:
18
Convention: Module constant names start with the dollar sign, $ for example, $pi. The dollar sign is part of the name, and does not have any special behavior. Both module variables and module constants are accessible within a module. (See Modules, for information about modules.) Dylan also offers variables that are accessible within a smaller area, called local variables. There is no concept of a local constant; all constants are module constants. Therefore, throughout the rest of this book, we use the word constant as shorthand for module constant. Local variables You can dene a local variable by using a let declaration. Unlike module variables, local variables are established dynamically, and they have lexical scope. During its lifetime, a local variable shadows any module variable, module constant, or existing local variable with the same name. Local variables are scoped within the smallest body that surrounds them. You can use let anywhere within a body, rather than just at the beginning; the local variable is declared starting at its denition, and continuing to the end of the smallest body that surrounds the denition. A body is a region of program code that delimits the scope of all local variables declared inside the body. When you are dening functions, usually there is an implicit body available. For example, define method creates an implicit body. (For information about method denitions, see Method denitions.) Other control structures, such as if, create implicit bodies. Bodies can be nested. If there is no body handy, or if you want to create a body smaller than the implicit one, you can create a body by using begin to start it and end to nish it:
? begin let radius = 5; let circumference = 2 \* $pi \* radius; circumference; end; => 31.4159
The local variables radius and circumference are declared, initialized, and used within the body. The value returned by the body is the value of the expression executed last in the body, which is circumference. Outside the lexical scope of the body, the local variables are no longer declared, and trying to access them is an error:
? radius => ERROR: The variable radius is undefined.
The format-out function sends output to the standard output destination, which could be the window where the program was invoked, or a new window associated with the program. The standard output destination depends on the platform.
19
The string argument can contain ordinary text, formatting instructions beginning with %, and characters beginning with a backslash, \. Ordinary text in the format string is sent to the destination verbatim. You can use the backslash character in the string argument to insert unusual characters, such as \n, which prints the newline character.
? format-out("Your future is filled with wondrous surprises.\n") => Your future is filled with wondrous surprises.
Formatting instructions begin with a percent sign, %. For each %, there is normally a corresponding argument giving an object to output. The character after the % controls how the object is formatted. A wide range of formatting characters is available, but we use only the following formatting characters in this book: %d Prints an integer represented as a decimal number %s Prints the contents of its string argument unquoted %= Prints an implementation-specic representation of the object; you can use %= for any class of object Here are examples:
? format-out ("Your number is %= and mine is %d\n", *your-number*, *my-number*); => Your number is 12.01 and mine is 7. ? format-out("The %s meeting will be held at %d:%d%d.\n", "Staff", 2, 3, 0); => The Staff meeting will be held at 2:30.
In Dylan, functions do not need to return any values. The format-out function returns no values. Thus, it is called only for its side effect (printing output). Comparison with C: format-out is similar to printf. The format-out function is available from the format-out library, and is not part of the core Dylan language. We now describe how to make the format-out function accessible to our program, and how to set up the les that constitute the program. Many of the details depend on the implementation of Dylan, so you will need to consult the documentation of your Dylan implementation. Usage note: The Apple Technology Release does not currently provide the format-out function. For information about how to run these examples in the Apple Technology Release, see Harlequins or Addison-Wesleys Web page for our book. See Resources on Dylan.
20
A Dylan library denes a software component a separately compilable unit that can be either a stand-alone program or a component of a larger program. Thus, when we talk about creating a Dylan program, we are really talking about creating a library. A library contains modules. Each module contains denitions and expressions. The module is a namespace for the denitions and expressions. For example, if you dene a module variable in one particular module, it is available to all the code in that module. If you choose to export that module variable, you can make it accessible to other modules that import it. In this chapter, we give the bare minimum of information about libraries and modules just enough for you to get started quickly. For a complete description of libraries and modules, see Libraries and Modules. To create a complete Dylan program, we need To dene the library that is our program; we shall create a library named hello To dene a module (or more than one) in the library, to hold the denitions and expressions in our program; we shall create a module named hello in the hello library To write the program code, in the module; we shall put the format-out expression in the hello module of the hello library Files of a Dylan program Different Dylan environments store programs in different ways, but there is a le-based interchange format that all Dylan environments accept. In this interchange format, any program consists of a minimum of two les: a le containing the program itself, and a le describing the libraries and modules. The most trivial program consists of a single module in a single library, but it is still expressed in two les. Most Dylan implementations also accept a third le, which enumerates all the les that make up a program; this le is called a library-interchange denition (LID) le. The details of how the les are named and stored depends on your Dylan implementation. Typically, however, you have a directory containing all the les of the program. As shown below, we name our program directory hello, and name the les hello.lid, library.dylan, and hello.dylan (the latter is the program le). hello hello.lid library.dylan hello.dylan Comparison with C: The following analogies may help you to understand how the elements of Dylan programs correspond to elements of C programs: The program les are similar to .c les in C. The library le is similar to a C header le. The LID le is similar to a makele, which is used in certain C development environments.
All Dylan expressions must be in a module. Therefore, we use a text editor to create a le that contains the expression within a module:
21
The hello.dylan le is the top-level le; you can think of it as the program itself. When you run this program, Dylan executes all the expressions in the le in the order that they appear in the le. There is only one expression in this program the call to format-out. The rst line of this le declares that the expressions and denitions in this le are in the hello module. Before we can run (or even compile) this program, we need to dene the hello module. All modules must be in a library, so we must also dene a library for our hello module. We create a second le, called the library le, and dene the hello module and hello library in the library le: The library le: library.dylan.
module: dylan-user define library hello use dylan; use format-out; end library hello; define module hello use dylan; use format-out; end module hello;
The rst line of library.dylan states that the expressions in this le are in the dylan-user module. Every Dylan expression and denition must be in a module, including the denitions of libraries and modules. The dylan-user module is the starting point the predened module that enables you to dene the libraries and modules that your program uses. In the le library.dylan, we dene a library named hello, and a module named hello. We dene the hello library to use the dylan library and the format-out library, and we dene the hello module to use the dylan module and the format-out module. One library uses another library to allow its modules to use the other librarys exported modules. Most libraries need to use the dylan library, because it contains the dylan module. One module uses another module to allow its denitions to use the other modules exported denitions. Most modules need to use the dylan module in the dylan library, because that module contains the denitions of the core Dylan language. We also need to use the format-out module in the format-out library, because that module denes the format-out function, which we use in our program. Finally, we create a LID le that enumerates the les that make up the library. This le does not contain Dylan expressions, but rather is simply a textual description of the librarys les: The LID le: hello.lid.
library: hello files: library hello
The LID le simply states that the library hello comprises two les, named library and hello. In other words, to build the hello library, the compiler must process the two les listed, in the order that they appear in the le. The order is signicant, because a module must be dened before the code that is in the module can be analyzed and compiled. You can consult the documentation of your Dylan implementation to nd out how to build an executable program from these les, and how to run that program once it is built. Most Dylan environments produce executable programs that 22 Chapter 3. Part 1. Basic Concepts
can be invoked in the same manner as any other program on the particular platform that you are using. We incur a fair amount of overhead in setting up the les that make up a simple program. Most environments automate this process some of the complexity shown here occurs because we are working with the lowest common denominator: interchange les. The advantages of libraries and modules are signicant for larger programs. See Libraries and Modules.
3.2.6 Summary
In this chapter, we covered the following: We entered Dylan expressions to a listener and saw their values or output. We used simple arithmetic functions: +, *, -. We used predicates: =, <, >, even?, and zero?. We described certain naming conventions in Dylan; see Dylan naming conventions shown in this chapter.. We described the syntax of some commonly used elements of Dylan; see Syntax of Dylan elements.. We dened module variables (with define variable), constants (with define constant), and local variables (with let). We set the value of variables by using :=, the assignment operator. We dened a simple but complete Dylan program, consisting of a LID le, a library le, and a program le. Here, we summarize the most basic information about libraries and modules: A Dylan library denes a software component a separately compilable unit that can be either a stand-alone program or a component of a larger program. Thus, when we talk about creating a Dylan program, we are really talking about creating a library. Each Dylan expression and denition must be in a module. Each module is in a library. One module uses another module to allow its denitions to use the other modules exported denitions. Most modules need to use the dylan module in the dylan library, because it contains the denitions of the core Dylan language. One library uses another library to allow its modules to use the other librarys exported modules. Most libraries need to use the dylan library, because it contains the dylan module. Table 3.1: Dylan naming conventions shown in this chapter. Dylan element module variable constant class predicate Example of name *my-number* $pi <integer> positive?
Table 3.2: Syntax of Dylan elements. Dylan element string true canonical true value false inx syntax function call function call Syntax example "Runway" any value that is not #f #t #f 2 + 3; max(2, 3);
23
We use define method to dene a method named say-hello. Just after the name say-hello, we specify the methods parameter list, (). The parameter list of this method is empty, meaning that this method takes no arguments. The call to say-hello provides an empty argument list, meaning that there are no arguments in the call. The body of the say-hello method has one expression a call to format-out. A method returns whatever is returned by the expression executed last in its body. In general, a method can return a single value, multiple values, or no value at all. The say-hello method returns what format-out returns no value at all. In the call to say-hello, we see the output of format-out in the listener; we see output and not a returned value (because no value is returned). Usage note: In this chapter, we dene methods that call the format-out function. Because format-out is in the format-out module, we need to make that module available. There are two ways to do so. The rst way is to work in les, as described in A complete Dylan program. The second way is to use a gesture or command in your Dylan environment to make the format-out module accessible. Then, you can simply enter the method denitions into the listener.
A method that takes an argument We can dene a method similar to say-hello, called say-greeting, that takes an argument:
define method say-greeting (greeting :: <object>); format-out("%s\n", greeting); end;
The say-greeting method has one required parameter, named greeting. The type constraint of the required parameter indicates the type that the argument must be. The greeting parameter has the type constraint <object>, which is the most general class. All objects are of the type <object>, so using this class as the type constraint allows the argument to be any object. You can omit the type constraint of a required parameter; that omission has the same effect as specifying <object> as the type constraint. We can call say-greeting on a string: 24 Chapter 3. Part 1. Basic Concepts
We can call say-greeting on an integer, although the integer does not give a particularly friendly greeting:
? define variable *my-number* :: <integer> = 7; ? say-greeting(*my-number*); => 7
Two methods with the same name For fun, we can change say-greeting to take a different action for integers, such as to print a message:
Your lucky number is 7.
To make this change, we dene another method, also called say-greeting. This method has one required parameter named greeting, which has the type constraint <integer>.
define method say-greeting (greeting :: <integer>) format-out("Your lucky number is %s.\n", greeting); end; ? say-greeting(*my-number*); => Your lucky number is 7.
A Dylan method is similar to a procedure or subroutine in other languages, but there is an important difference. You can dene more than one method with the same name. Each one is a method for the same generic function. The say-greeting generic function and its methods shows how you can picture a generic function. When a generic function is called, it chooses the most appropriate method to call for the arguments. For example, when we call the say-greeting generic function with an integer, the method whose parameter is of the type <integer> is called:
? say-greeting(1000); => Your lucky number is 1000.
When we call the say-greeting generic function with an argument that is not an integer, the method whose parameter is of the type <object> is called:
? say-greeting("Buenos Dias"); => Buenos Dias define method say-greeting (greeting :: <object>) format-out("%s\n", greeting); end; define method say-greeting (greeting :: <integer>) format-out("Your lucky number is %s.\n", greeting); end;
3.3.2 Classes
We have already seen examples of classes in Dylan: <integer>, <single-float>, <string>, and <object>.
25
Individual values are called objects. Each object is a direct instance of one particular class. You can use the object-class function to determine the direct class of an object. For example, in certain implementations, 7, 12, and 1000 are direct instances of the class <integer>:
? object-class(1000); => {class <integer>}
The value returned by object-class is the <integer> class itself. The appearance of a class, method, or generic function in a listener depends on the Dylan environment. We have chosen a simple appearance of classes for this book. All the classes that we have seen so far are built-in classes, provided by Dylan. In User-Dened Classes and Methods, we show how to dene new classes. Class inheritance One important aspect of classes is that they are related to one another by inheritance. Inheritance enables classes that are logically related to one another to share the behaviors and attributes that they have in common. Each class inherits from one or more classes, called its superclasses. If no other class is appropriate, then the class inherits from the class <object>. This class is the root of all classes: All classes inherit from it, either directly or indirectly, and it does not have any direct superclasses. Comparison with C++: If you are familiar with the class concepts of C++, you might initially be confused by Dylans class model. In Dylan, all base classes are effectively virtual base classes with virtual data members. When a class inherits another class more than once (because of multiple inheritance), only a single copy of that base class is included. Each of the multiple-inheritance paths can contribute to the implementation of the derived class. The Dylan class model favors this mix-in style of programming. For more information, see The concept of classes in Dylan Object Model for C and C++ Programmers. In Dylan, we distinguish between two terms: direct instance and general instance. An object is a direct instance of exactly one class: the class that object-class returns for that object. An object is a general instance of its direct class, and of all classes from which its direct class inherits. The term instance is equivalent to general instance. You can use the instance? predicate to ask whether an object is an instance of a given class:
? instance?(1000, <integer>); => #t ? instance?("hello, world", <integer>); => #f
Classes and subclasses. shows the inheritance relationships among several of the built-in classes. If class A is a superclass of class B, then class B is a subclass of class A. For example, <object> is a superclass of <string>, and <string> is a subclass of <object>. For simplicity, Classes and subclasses. omits certain classes that intervene between the classes shown. A typical Dylan environment provides a browser to explore inheritance relationships among classes; certain environments show the relationships graphically.
26
The Dylan language includes functions that provide information about the inheritance relationships among classes. We can use subtype? to ask whether one class inherits from another class:
? subtype?(<integer>, <number>); => #t ? subtype?(<integer>, <object>); => #t ? subtype?(<single-float>, <object>); => #t ? subtype?(<string>, <integer>); => #f
It may be confusing that we use a function called subtype? here, but Dylan does not provide a function called subclass?. Every class is a type, but certain types are not classes (see Functions that create nonclass types). The subtype? function works for both classes and other types. We can ask for all the superclasses of a given class:
? all-superclasses(<string>); => #[{class <string>}, {class <mutable-sequence>}, {class <sequence>}, => {class <mutable-collection>}, {class <collection>}, {class <object>}] ? all-superclasses(<integer>); => #[{class <integer>}, {class <rational>}, {class <real>}, => {class <number>}, {class <object>}] ? all-superclasses(<single-float>); => #[{class <single-float>}, {class <float>}, {class <real>}, => {class <number>}, {class <object>}]
The all-superclasses function returns a vector containing the class itself and all that classs superclasses. The #[...] syntax represents a vector, which is a one-dimensional array. (For information about vectors, see Collections and Control Flow.) 3.3. Methods, Classes, and Objects 27
Relationship between classes and methods The relationship between classes and methods in Dylan is different from that in C++ and Smalltalk, among other languages. Comparison to C++ and Smalltalk: In C++ and Smalltalk, a class contains the equivalent of methods. In Dylan, a class does not contain methods; instead, a method belongs to a generic function. This design decision enables these powerful features of Dylan: You can dene methods on built-in classes (because you do not have to modify the class denition to dene a method intended for use on the class). For an example, see Methods for the + generic function. More generally, you can dene a method for a class that you did not dene. You can write multimethods. In a multimethod, the method dispatch is based on the classes of more than one argument to a generic function. For an introduction to method dispatch, see Method dispatch. For information about multimethods, see Multimethods. You can restrict generic functions to operate on specic classes of objects. In Dylan, a method belongs to a generic function, as shown in The say-greeting generic function and its methods. Although methods are independent of classes, methods operate on instances of classes. A method states the types of objects for which it is applicable by the type constraint of each of its required parameters. Consider the say-greeting method dened earlier:
define method say-greeting (greeting :: <integer>); format-out("Your lucky number is %s.\n", greeting); end;
This method operates on instances of the <integer> class. Notice how easy and convenient it is to dene a method intended for use on the built-in class <integer>.
3.3.3 Objects
In Dylan, everything is an object. Characters, strings, numbers, arrays, and vectors are all objects. The canonical true and false values, #t, and #f, are objects. Methods, generic functions, and classes are objects. What does it mean to be an object? Most important, an object has a unique identity. You can use the == predicate to test whether two operands are the same object. See Predicates for testing equality. An object is a direct instance of a particular class. You can use the object-class predicate to determine the direct class of an object. You can give an object a name. For example, if you dene a variable or constant to contain an object, you have given that object a name. See Bindings: Mappings between objects and names. You can pass an object as an argument or return value because generic functions and methods are objects, you can manipulate them just as you can any other object. See Functions as objects. Comparison to C++ and Smalltalk: In Dylan and Smalltalk, everything is an object (an instance of a class); we say that Dylan and Smalltalk have objects all the way down. In contrast, in C++, some values are not objects; they have primitive types that are not classes. For example, in Dylan, 7 is an instance of <integer>. In C++, 7 is not an instance; it has the type int. This design decision enables Dylan users to dene methods on built-in classes in the same way that they dene methods on user-dened classes a technique that cannot be done in C++.
28
Comparison to Java: Java recognizes the need for object representation of all classes with the Number class and its subclasses. However, Java still requires the programmer to work with nonobjects when writing mathematical statements. The Number classes can be used to wrap an object cloak around the primitive integer, float, and other numeric types, to allow object-based programming. Dylan does not separate the mathematical manipulation of numbers from their other object properties. Programmers need only to think in terms of numerical objects, and can rely on the compiler to implement mathematical operations efciently. Similarly, the Boolean class is used to encapsulate primitive boolean values as objects, and programmers must convert back and forth, depending on the context.
Predicates for testing equality Dylan provides two predicates for testing equality: = and ==. The = predicate determines whether two objects are similar. Similarity is dened differently for different kinds of objects. When you dene new classes, you can dene how similarity is tested for those classes by dening a method for =. The == predicate determines whether the operands are identical that is, whether the operands are the same object. The == predicate (identity) is a stronger test: two values may be similar but not identical, and two identical values are always similar. If two numbers are mathematically equal, then they are similar:
? 100 = 100; => #t ? 100 = 100.0; => #t
Two numbers that are similar, and have the same type, are the same object:
? 100 == 100; => #t
Two numbers that are similar, but have different types, are not the same object:
? 100 == 100.0; => #f
Characters are enclosed in single quotation marks. If two characters look the same, they are similar and identical:
? z = z; => #t ? z == z; => #t
Strings are enclosed in double quotation marks. Strings that have identical elements are similar, but may or may not be identical. That is, strings can have identical elements, but not be the same string. For example, these strings are similar:
? "apple" = "apple"; => #t
Just by looking at two strings, you cannot know whether or not they are the identical string. The only way to determine identity is to use the == predicate. The following expression could return #t or #f:
29
? "apple" == "apple";
Bindings: Mappings between objects and names A binding is a mapping between an object and a name. The name can be a module variable, module constant, or local variable. Here, we give the object 3.14159 the name $pi, where $pi is a module constant:
? define constant $pi = 3.14159;
Here, we give the object "apple" the name *my-favorite-pie*, where *my-favorite-pie* is a module variable:
? define variable *my-favorite-pie* = "apple";
More than one variable can contain a particular object, so, in effect, an object can have many names. Here, we dene a new variable that contains the very same pie:
? define variable *your-favorite-pie* = *my-favorite-pie*; ? *your-favorite-pie* == *my-favorite-pie*; => #t
When you dene a method, define method creates a binding between a name and a method object:
? define method say-greeting (greeting :: <object>); format-out("%s\n", greeting); end;
All the bindings that we have created in this section so far are accessible within a module. (For information about modules, see Libraries and Modules.) Bindings as links shows how you can picture each binding as a link between a name and another object. Local variables are also bindings, but they are accessible only within a certain body of code; for example,
? begin let radius = 5.0; let circumference = 2.0 * $pi * radius; circumference; end;
Bindings can be constant or variable. You can use the assignment operator to change a variable binding, but you cannot change a constant binding. Module constants are constant bindings; module variables and local variables are variable bindings.
3.3.4 Summary
In this chapter, we covered the following: 30 Chapter 3. Part 1. Basic Concepts
Figure 3.4: Bindings as links (shown as arrows) between names (enclosed in ovals) and objects (enclosed in rectangles) within a module. A generic function can contain more than one method, where each method has parameters of different types, and thus is intended for different arguments. The say-greeting generic function has two methods. Dylan provides built-in classes, including <integer>, <single-float>, <string>, and <object>. These classes are related by inheritance. In Dylan, almost everything is an object. Each object has a unique identity. The = predicate tests for similarity; the == predicate tests for identity. A binding is an association between an object and a name.
31
The top line is a comment. The // characters begin a comment, which continues to the end of the line. We also provide comments that number the lines of code after the rst comment. The line numbers are useful only for discussing the code examples in the book, and would not be used in source les. You can also have multi-line comments that start with /* and end with */. On line 1, the words define class start the class denition. The name of the class is <time-of-day>. The list following the name of the class is a list of the direct superclasses of this class. The <time-of-day> class has one direct superclass, which is the class <object>. Each user-dened class must have at least one direct superclass. If no other class is appropriate, the class must have <object> as its superclass. Line 2 contains the only slot denition of this class. This class has one slot, named total-seconds. The slots type constraint is <integer>. The double colon, ::, species the type constraint of a slot, just as it species the type constraint of a module variable or of a methods parameter. Line 3 is the end of the class denition. The text after the word end and before the semicolon is an optional part of the denition; it documents which denition is ending. Any text appearing after the end must match the denition ending, such as end class <time-of-day>, or end class. You do not need to put any text after the end however, such text is useful for long or complex denitions, where it can be difcult to see which language construct is ending. The type constraint of a slot The type constraint of the total-seconds slot is <integer>. This slot can hold instances of <integer>, and cannot hold any other kind of object. The type constraint of a slot is optional. Specifying a slot with no explicit type constraint is equivalent to specifying <object> as the type constraint. A slot whose type constraint is <object> can hold any object. The ability to have slots with the type constraint <object> provides exibility that can be valuable; for more information, see Performance and Flexibility.
The make function creates an instance of <time-of-day>. The argument to make is the class to create. The make function returns the new instance.
32
The instance stored in *my-time-of-day* has a total-seconds slot with no value. The next logical step is to store a value in that slot.
Although these expressions may look like they are accessing the slots directly, they are not. They are abbreviations for function calls to a getter and a setter. A getter is a method that retrieves the current value of a slot in an object. A setter is a method that stores a value in a slot. Each slot in a class automatically has a getter and a setter dened for it. You can see the function-call syntax, and other information about getters and setters, in Slots.
We will be able to make this call after we have done a bit of homework, as we shall show in Init keywords: Keywords that initialize slots. In the preceding call to make, we provided a keyword argument, consisting of a keyword, total-seconds:, followed by a value, 120. The <time-of-day> instance returned by make has its total-seconds slot set to 120. A keyword argument consists of a keyword followed by the keywords value. A keyword is a name followed by a colon, such as total-seconds:. The colon after a keyword is not a convention; it is a required part of the keyword. There must be no space between the name and the colon. You can dene functions to accept keyword arguments. When a function accepts keyword arguments, you can provide them in any order. Keyword arguments can be useful for functions that take many arguments when you call the function, you do not need to remember the order of the arguments. Keyword arguments are optional arguments, so they are useful for parameters that have a default value that you may want to override at times. For more information about keyword arguments, see Parameter lists. How does make know that the value of the total-seconds: keyword should be used to initialize the total-seconds slot? The keyword and the slot happen to have the same name, but that is not how it knows. Before you can use the total-seconds: keyword argument to make, you must associate that keyword with the total-seconds slot in the class denition.
33
Init keywords: Keywords that initialize slots The total-seconds: keyword is an init keyword a keyword that we can give to make to provide an initial value for a slot. To make it possible to give an init keyword to make, we need to use the init-keyword: slot option when we dene the class. A slot option lets us specify a characteristic of a slot. Slot options appear after the optional type specier of a slot. Here, we redene the <time-of-day> class to use the init-keyword: slot option:
1 2 3 4
// A specific time of day from 00:00 (midnight) to below 24:00 (tomorrow) define class <time-of-day> (<object>) slot total-seconds :: <integer>, init-keyword: total-seconds:; end class <time-of-day>;
The preceding denition redenes the class <time-of-day>. That is, this new denition of <time-of-day> replaces the old denition of <time-of-day>. In line 3, the init-keyword: slot option denes total-seconds: as a keyword parameter that we can give to make when we make an instance of this class. Now that we have dened total-seconds: as an init keyword, we can provide the keyword argument as follows:
? *my-time-of-day* := make(<time-of-day>, total-seconds: 120); => {instance of <time-of-day>}
The preceding expression creates a new instance of <time-of-day>, and stores that instance in the variable *my-time-of-day*. The value of the total-seconds slot of this instance is initialized to 120. The assignment operator returns the new value stored; in the preceding call, the new value is the newly created instance of <time-of-day>, which the listener displays as {instance of <time-of-day>}. We can use the getter to verify that the slot has an initial value:
? *my-time-of-day*.total-seconds; => 120
If you call make and provide a keyword that has not been declared as a valid keyword for the class, you get an error; for example,
? make(<time-of-day>, seconds: 120); => ERROR: seconds: is not a valid keyword argument to make for {class <time-of-day>}
Automatic storage-management note: Dylan provides automatic storage management (also called garbage collection). Thus, you do not need to deallocate memory explicitly. When an object becomes inaccessible, Dylans automatic storage management will recycle the storage used by that object. In this section, there are two examples of objects that become inaccessible: We redened the <time-of-day> class. The storage used by the old class denition can be recycled. We stored a new instance in *my-time-of-day*. The storage used by the instance previously stored in that variable can be recycled. Although redenition is not part of the Dylan language, most Dylan development environments support redenition.
34
Comparison with Java: Java recognizes that manual memory management can be the source of program errors and often can be exploited to breach security measures. Like Dylan, Java has an automatic garbage collector that correctly and efciently recovers unused objects in a program freeing the programmer of that mundane but difcult chore.
define method encode-total-seconds (hours :: <integer>, minutes :: <integer>, seconds :: <integer>) => (total-seconds :: <integer>) ((hours * 60) + minutes) * 60 + seconds; end method encode-total-seconds;
Line 2 contains the parameter list of the method encode-total-seconds. The method has three required parameters, named hours, minutes, and seconds, each of type <integer>. This method is invoked when encode-total-seconds is called with three integer arguments. Line 3 contains the value declaration, which starts with the characters =>. It is a list declaring the values returned by the method. Each element of the list contains a descriptive name of the return value and the type of the value (if the type is omitted, it is <object>). In this case, there is one value returned, named total-seconds, which is of the type <integer>. The name of a return value is used purely for documentation purposes. Although methods are not required to have value declarations, there are advantages to supplying those declarations. When you provide a value declaration for a method, the compiler signals an error if the method tries to return a value of the wrong type, can check receivers of the results of the method for correct type, and can usually produce more efcient code. These advantages are signicant, so we use value declarations throughout the rest of this book. For more information about value declarations, see Value declarations. Line 4 is the only expression in the body. It uses arithmetic functions to convert the hours, minutes, and seconds into total seconds. All methods return the value of the expression executed last in the body. This method returns the result of the arithmetic expression in line 4. In line 5, we could have simply used end;. We provided end method decode-total-seconds; for documentation purposes. Throughout the rest of this book, we provide the extra words after the end of a denition. We can call encode-total-seconds with arguments representing 8 hours, 30 minutes, and 59 seconds:
? encode-total-seconds(8, 30, 59); => 30659
We nd it convenient to call encode-total-seconds to initialize the total-seconds slot when we create an instance of <time-of-day>, or when we store a new value in that slot. Here, for example, we create a new instance: 3.4. User-Dened Classes and Methods 35
The result reminds us that it would be useful to convert in the other direction as well from total seconds to hours, minutes, and seconds. Method for decode-total-seconds We dene decode-total-seconds to convert in the other direction from total seconds to hours, minutes, and seconds:
1 2 3 4 5 6 7
define method decode-total-seconds (total-seconds :: <integer>) => (hours :: <integer>, minutes :: <integer>, seconds :: <integer>) let (total-minutes, seconds) = truncate/(total-seconds, 60); let (hours, minutes) = truncate/(total-minutes, 60); values(hours, minutes, seconds); end method decode-total-seconds;
The value declaration on line 3 species that decode-total-seconds returns three separate values: the hours, minutes, and seconds. This method illustrates how to return multiple values, and how to use let to initialize multiple local variables. We describe these techniques in Sections Multiple return values and Use of let to declare local variables. Multiple return values The method for decode-total-seconds returns three values: the hours, the minutes, and the seconds. To return the three values, the method uses the values function as the expression executed last in the body. The values function simply returns all its arguments as separate values. The ability to return multiple values allows a natural symmetry between encode-total-seconds and decode-total-seconds, as shown in symmetryof-encode-decode. Table 3.3: Symmetry of encode-total-seconds and decode-total-seconds. Method encode-total-seconds decode-total-seconds Parameter(s) hours, minutes, seconds total-seconds Return value(s) total-seconds hours, minutes, seconds
Lines 4 and 5 of the decode-total-seconds method contain calls to truncate/. The truncate/ function is a built-in Dylan function. It takes two arguments, divides the rst by the second, and returns two values: the result of the truncating division, and the remainder.
36
Comparison with C: In C, / on integers produces a truncated result. In Dylan, / on integers is implementation dened, and is not recommended for portable code. The Dylan functions named floor, ceiling, round, and truncate convert a rational or oating-point result to an integer with the appropriate rounding. The Dylan functions named floor/, ceiling/, round/, and truncate/ take two arguments. Those generic functions divide the rst argument by the second argument, and return two values: the rounded or truncated result, and the remainder.
Use of let to declare local variables When a function returns multiple values, you can use let to store each returned value in a local variable, as shown in lines 2 and 3 of the decode-total-seconds method in Method for decode-total-seconds. On line 2, we use let to declare two local variables, named total-minutes and seconds, and to initialize their values to the two values returned by the truncate/ function. Similarly, on line 3, we use let to declare the local variables hours and minutes. The local variables declared by let can be used within the method until the methods end. Although there is no begin to dene explicitly the beginning of a body for local variables, define method begins a body, and its end nishes that body. Local variables are scoped within the smallest body that surrounds them, so you can use begin and end within a method to dene a smaller body for local variables, although doing so is usually not necessary. Second method for decode-total-seconds The decode-total-seconds method is called as follows:
? decode-total-seconds(*your-time-of-day*.total-seconds);
If we envision calling decode-total-seconds frequently to see the hours, minutes, and seconds stored in a <time-of-day> instance, we can make it possible to decode <time-of-day> instances, as well as integers. For example, we can make it possible to make this call:
? decode-total-seconds(*your-time-of-day*);
We can implement this behavior easily, by dening another method for decode-total-seconds, which takes a <time-of-day> instance as its argument:
define method decode-total-seconds (time :: <time-of-day>) => (hours :: <integer>, minutes :: <integer>, seconds :: <integer>) decode-total-seconds(time.total-seconds); end method decode-total-seconds;)
and its methods shows the two methods for the The decode-total-seconds generic function and its
// Method on <integer> define method decode-total-seconds (total-seconds :: <integer>) => (hours :: <integer>, minutes :: <integer>, seconds :: <integer>) let (total-minutes, seconds) = truncate/(total-minutes, 60); values(hours, minutes, seconds); end method decode-total-seconds;
37
// Method on <time-of-day> define method decode-total-seconds (time :: <time-of-day>) => (hours :: <integer>, minutes :: <integer>, seconds :: <integer>) decode-total-seconds(time.total-seconds); end method decode-total-seconds;
Looking at The decode-total-seconds generic function and its methods, we analyze what happens in this call:
? decode-total-seconds(*your-time-of-day*);
1. The argument is an instance of <time-of-day>, so the method on <time-of-day> is called. 2. The body of the method on <time-of-day> calls decode-total-seconds on an instance of <integer>, the value of the total-seconds slot of the <time-of-day> instance. In this call, the argument is an integer, so the method on <integer> is called. 3. The method on <integer> returns three values to its caller the method on <time-of-day>. The method on <time-of-day> returns those three values. The purpose of the method on <time-of-day> is simply to allow a different kind of argument to be used. The method extracts the integer from the <time-of-day> instance, and calls decode-total-seconds with that integer. Method for say-time-of-day We can provide a way to ask an instance of <time-of-day> to describe the time in a conventional format, such as 8:30. For the application that we are planning, there is no need to view the seconds. We want the method to print the description in a window on the screen. We dene a method named say-time-of-day:
1 2 3 4 5
define method say-time-of-day (time :: <time-of-day>) => () let (hours, minutes) = decode-total-seconds(time); format-out ("%d:%s%d", hours, if (minutes < 10) "0" else "" end, minutes); end method say-time-of-day;
On line 1, we provide an empty value declaration, which means that this method returns no values. On line 2, we use let to initialize two local variables to the rst and second values returned by decode-total-seconds. Remember that decode-total-seconds returns three values (the third value is the seconds). For the application that we are planning, the say-time-of-day method does not need to show the seconds, so we do not use the third value. It is not necessary to receive the third value of decode-total-seconds; here we do not provide a local variable to receive the third value, so that value is simply ignored. On line 4, we use if to print a leading 0 for the minutes when there are fewer than 10 minutes, such as 2:05. Comparison to C: In C, if does not return a value. In Dylan, if returns the value of the body that is selected, if any is.
Note on format-out: We have purposely used a limited subset of the format-out functions features to allow our examples to run on as many Dylan implementations as possible. The printing of times could be done much more elegantly if we used the full power of the format-out function.
38
The listener displays the output (printed by format-out), but displays no values, because say-time-of-day does not return any values.
3.4.7 Summary
In this chapter, we covered the following: We dened a class (with define class). We created an instance (with make). We read the value of a slot by calling a getter. We set the value of a slot by using :=, the assignment operator. We dened a method that returns multiple values (with values), and showed how to initialize multiple local variables (with let). We showed the syntax of some commonly used elements of Dylan; see Syntax of Dylan elements.. Table 3.4: Syntax of Dylan elements. Dylan element calling a getter calling a setter keyword single-line comment multiline comment value declaration Syntax example *my-time-of-day*.total-seconds; *my-time-of-day*.total-seconds := 180; total-seconds: // Text of comment /* Text of comment that spans more than one line */ => (total-seconds :: <integer>)
39
Reasons for dening two similar classes The <time-offset> class is similar to the <time-of-day> class. They both dene a total-seconds slot. Why do we need to have two classes that are so similar? A <time-of-day> is conceptually different from a <time-offset>. If the total-seconds slot of a <time-of-day> is 180, that means the time of day at 0:03 (that is, 3 minutes past midnight). If the total-seconds slot of a <time-offset> is 180, that means 3 minutes in the future. If you ask what time it is, the answer is a <time-of-day>. If you ask how long it takes to wash the dog, the answer is a <time-offset>. A <time-offset> can represent time in the past by having a negative value of total-seconds. A <time-of-day>, in contrast, should not have a negative value of total-seconds. Later in this book, we provide methods that guarantee that the total-seconds slot of <time-of-day> instances is not negative; see Setter methods, and Initialize methods. We need different methods for describing instances of <time-offset> and instances of <time-of-day>. The <time-of-day> method prints 8:30, and the <time-offset> method should print minus 8:30 or plus 8:30. Eventually, we will need to be able to add a <time-of-day> to a <time-offset>. For example, we can add the <time-of-day> 9:03 to the <time-offset> 2:50 and get the <time-of-day> 11:53. We will also need to add two <time-offset> instances. For example, 2 minutes plus 8 minutes is equal to 10 minutes. But we cannot add two <time-of-day> instances, because it does not make sense to add three oclock to four oclock. Creation of instances of <time-offset> We can create an instance of <time-offset> representing 15:20:10 in the future:
? define variable *my-time-offset* :: <time-offset> = make(<time-offset>, total-seconds: encode-total-seconds(15, 20, 10));
We can create an instance of <time-offset> representing 6:45:30 in the past, by using the unary minus function, -, which returns the negative of the value that follows it:
? define variable *your-time-offset* :: <time-offset> = make(<time-offset>, total-seconds: - encode-total-seconds(6, 45, 30));
Methods on <time-offset> Because a <time-offset> can represent future time or past time, it will be useful to provide a convenient way to determine whether a <time-offset> is in the past. We dene a new predicate named past? as follows:
define method past? (time :: <time-offset>) => (past? :: <boolean>) time.total-seconds < 0; end method past?;
The past? method returns an instance of <boolean>, which is #t if the time offset is in the past, and otherwise is #f. Here is an example:
? past?(*my-time-offset*) => #f ? past?(*your-time-offset*) => #t
40
We need a method to describe instances of <time-offset>. The output should look like this:
? say-time-offset(*my-time-offset*); => plus 15:20 ? say-time-offset(*your-time-offset*); => minus 6:45
No applicable method means that there is no method for this generic function that is appropriate for the arguments. To understand this error, we can look at the methods for decode-total-seconds in Second method for decodetotal-seconds. One method takes an argument of the type <integer>. Another method takes an argument of the type <time-of-day>. There is no method for instances of <time-offset>, so Dylan signals an error. There are three possible approaches to solving this problem. As a rst approach, we could dene the say-time-offset method to call decode-total-seconds with an integer.
1 2 3 4 5 6 7 8 9
// First approach: Call decode-total-seconds with an integer define method say-time-offset (time :: <time-offset>) => () let (hours, minutes) = decode-total-seconds(abs(time.total-seconds)); format-out("%s %d:%s%d", if (past?(time)) "minus" else "plus" end, hours, if (minutes < 10) "0" else "" end, minutes); end method say-time-offset;
We changed only the call to decode-total-seconds on line 3. Here, we call it with the absolute value (returned by the abs function) of the total-seconds slot. This approach works, but it is awkward because we need to remember what kinds of arguments decode-total-seconds can take. The convenient calling syntax that we introduced for calling decode-total-seconds with an instance of <time-of-day> is not available for other kinds of time. As a second approach, we could to dene a third method for decode-total-seconds that takes as its argument an instance of <time-offset>:
// Second approach: Define a method on <time-offset> define method decode-total-seconds (time :: <time-offset>) => () decode-total-seconds(abs(time.total-seconds)); end method decode-total-seconds;
The method for say-time-offset can then call decode-total-seconds, as we did in the rst place:
41
define method say-time-offset (time :: <time-offset>) => () let (hours, minutes) = decode-total-seconds(time); format-out("%s %d:%s%d", if (past?(time)) "minus" else "plus" end, hours, if (minutes < 10) "0" else "" end, minutes); end method say-time-offset;
This approach works, and it preserves the exibility of calling decode-total-seconds on instances of <integer>, <time-of-day>, and <time-offset>. However, the body of the method on <time-offset> (dened in this section) is nearly identical to the body of the method on <time-of-day> (dened in Second method for decode-total-seconds). The only difference is that we use abs in the method on <time-offset> but not in the method on <time-of-day>. If we used it in the method on <time-of-day>, it would be harmless. Duplication of code is ugly, adds maintenance overhead, and is particularly undesirable when programming in an object-oriented language, where it may indicate a aw in the overall design. The best solution to the problem lies in a third approach to rethink the classes and methods in a more object-oriented style, using inheritance. We show this solution in the next section.
There is commonality between the two classes: Both classes represent a kind of time they have a conceptual basis in common. Both classes have a total-seconds slot they have structure in common. Both classes need a decode-total-seconds method to convert the total-seconds slot to hours, minutes, and seconds they have behavior in common. We can use inheritance to model the shared aspects of these two classes directly. We need to dene a new class, such as <time>, and to redene the two classes to inherit from <time>. The <time> class will contain the slot total-seconds, and the other two classes will inherit that slot. We shall redene the decode-total-seconds method such that its parameter is of the <time> type, which means that it can be called for instances of <time-of-day> and of <time-offset>. New denitions of the time classes We dene the new class <time>:
define class <time> (<object>) slot total-seconds :: <integer>, init-keyword: total-seconds:; end class <time>;
42
Dynamic feature no need to recompile: In C++, a complete recompile of the program would be necessary to change the superclass of a class. Most Dylan development environments support a mode that requires only that you compile the new class denitions. The difference between compiling only a few class denitions and compiling the whole program can be a time saver for complex applications.
Slot inheritance A class inherits the slots of its superclasses, and can dene more slots if they are needed. For example, the <time-of-day> and <time-offset> classes inherit the total-seconds slot from their superclass, <time>. A class inherits the slot options from its superclasses as well. A class cannot remove or replace any slots dened by its superclasses. It is an error for a class to dene a slot with the same name as a slot inherited from one of that classs superclasses. Existing instances of the classes The variables and *my-time-of-day*, *your-time-of-day*, *my-time-offset*, your-time-offset* all contain instances of classes that have now been redened. Some environments * might be able to update instances of the old class denitions to conform to the new class denitions, but we will be conservative and assume that our environment does not update instances. Therefore, we create the instances again:
? *my-time-offset* := make(<time-offset>, total-seconds: encode-total-seconds(15, 20, 10)); ? *your-time-offset* := make(<time-offset>, total-seconds: - encode-total-seconds(6, 45, 30)); ? *my-time-of-day* := make(<time-of-day>, total-seconds: 120); ? *your-time-of-day* := make(<time-of-day>, total-seconds: encode-total-seconds(8, 30, 59));
Relationships of the time classes It is helpful to look at the relationships among the time classes. We show them in Inheritance relationships of the time classes.. Referring to Inheritance relationships of the time classes., we introduce terminology by example: The <time-of-day> class is a direct subclass of the <time> class. The <time-of-day> class is a subclass of the <object> class.
43
The <time> class is a direct superclass of the <time-of-day> class. The <object> class is a superclass of the <time-of-day> class. When you make an instance of the <time-of-day> class, the result is a direct instance of that class.
Figure 3.5: Inheritance relationships of the time classes. A direct instance of <time-of-day> is an indirect instance of <time> and <object>. An object is a general instance of a class if it is either a direct or an indirect instance of that class. The term instance is equivalent to general instance. A direct instance of <time-of-day> is both a general instance and an instance of <time-of-day>, <time>, and <object>. The <time-of-day> class is a subtype of the <time> and <object> classes. A class is also a subtype of itself. All classes are types. The <object> class is a supertype of all the other classes shown. All classes are subtypes of the <object> class. All objects are instances of the <object> class.
44
To take advantage of the redened classes, we want to remove the method on <time-of-day>, and to add a method on <time>. The method on <time> is appropriate for instances of both <time-of-day> and <time-offset>. There are two important points to cover. We rst discuss how to remove the method on <time-of-day> and how to add the method on <time> in Redenition of a method. We then describe how the decode-total-seconds generic function works in Method dispatch.
Assume that we are working in a listener, and already have dened the methods shown in Existing methods for decodetotal-seconds. Consider what happens when we dene the method on <time>. The parameter list of the new method is not equivalent to the parameter list of any of the existing methods, so the new method is added to the generic function. Thus, decode-total-seconds has three methods: a method on <integer>, a method on <time-of-day>, and a method on <time>. The environment may offer a way to remove a method from a generic function. When we remove the denition of the method on <time-of-day> using the environment, the decode-total-seconds generic function contains only the desired methods, as shown in Desired methods for decode-total-seconds. A typical browser will help you to nd the methods to remove. If, however, we are working in source les rather than in a listener, we simply need to remove the method on <time-of-day> with the editor, and to type in the method on <time>. When we next compile the le, the generic function will contain only the desired methods, as shown in Desired methods for decode-total-seconds. We can now call decode-total-seconds on instances of <time-of-day> and on instances of <time-offset>:
45
The result is as expected decode-total-seconds returns the hours, minutes, and seconds. We now describe how this generic function works.
The rst row of the table shows that, when the argument is a direct instance of <time-of-day>, the method on <time> is applicable, because the argument is an instance of <time> (the methods parameter specializer). The nal row of the table shows that, when the argument is "hello, world", none of the dened methods are applicable, because "hello, world" is not an instance of <time> or <integer>. For decode-total-seconds, there is either no or one applicable method for any argument. If there is one applicable method, it is called. If there is no applicable method, the No applicable method error is signaled. There is no need to continue to step 2.
46
In other cases, there can be several applicable methods. Consider the generic function say-greeting, shown in The say-greeting generic function and its methods. Applicable methods for different arguments to say-greeting. shows that, for certain arguments, one method is applicable, but that, for an integer argument, two methods are applicable. When the argument is 7, a direct instance of <integer>, the method on <object> is applicable, because 7 is an instance of <object> (the methods parameter specializer); the method on <integer> also is applicable, because 7 is an instance of <integer> (the methods parameter specializer). The say-greeting generic function and its methods:
define method say-greeting (greeting :: <object>) format-out("%s\n", greeting); end; define method say-greeting (greeting :: <integer>) format-out("Your lucky number is %s.\n", greeting); end;
Table 3.6: Applicable methods for different arguments to say-greeting. Argument 7 Applicable method(s) 1. method on <object> 2. method on <integer> method on <object> method on <object>
Start with the set of applicable methods. Compare the parameter specializers of the methods. If one type is a subtype of the other, the method whose parameter is of the subtype is more specic than the other method. Sort the list of applicable methods from most specic to least specic. Lets continue with the example of calling say-greeting with an argument of 7. The parameter specializers of the two methods are <object> and <integer>. Because <integer> is a subtype of <object>, the method on <integer> is more specic than the method on <object>. Step 3: Call the most specic method The generic function calls the most specic method. Precedence in method dispatch This conceptual description of how method dispatch works should help you to understand how to design methods. The most important concept to realize is that method dispatch should feel natural it gives precedence to the methods that are more closely related to the argument, rather than to the methods that are more general. This precedence ordering lets you adjust the behavior of a class with respect to that classs superclasses.
47
Performance note: The Dylan compiler and run-time system ensure that the method-dispatch rules are followed for every call to a generic function. Given accurate type declarations, however, a compiler can usually compute the result of the dispatch rules at compile time, so the executed code is just as efcient as a normal function call in a language without generic functions and methods. See Performance and Flexibility.
Now that decode-total-seconds has an applicable method for instances of <time-offset> and <time-of-day>, both these methods work correctly:
? say-time-of-day(*my-time-of-day*); => 0:02 ? say-time-of-day(*your-time-of-day*); => 8:30 ? say-time-offset(*my-time-offset*); => plus 15:20 ? say-time-offset(*your-time-offset*); => minus 6:45
We have dened two methods: say-time-offset and say-time-of-day. A method dened with define method cannot exist without a generic function. When you dene a method, and no generic function of that name exists, Dylan automatically creates a generic function. When we dened these two methods, there were no generic functions with those names dened, so Dylan created module variables named say-time-of-day and say-time-offset, created the generic functions, stored the generic functions in the module variables, and added the methods to the generic functions. These two methods are logically related to each other, but have no explicit relationship in the code, other than in the similarity of their names. A cleaner approach is to abstract the concept of what these methods are trying to do that is, to describe an object. To introduce this abstraction, we dene a new generic function. We use define generic to dene the generic function explicitly:
// Given an object, print a description of the object define generic say (any-object :: <object>) => ();
48
This generic function has a name: say. It receives one argument: the object to describe. That argument must be of the type <object>. All objects are of the type <object>, so this generic function does not restrict the type of its argument. Our denition for the generic function say is similar to that of the generic function that Dylan would have created automatically if we had dened a method for say before we dened the generic function say . (The only difference is that the automatically dened generic function would have a more general value declaration.) However, dening the generic function explicitly enables us to formalize its purpose, to name the parameter, to specify a type constraint on the parameter, to specify the return values and their types, and to give comments about the generic function as a whole. The generic function denes the contract that all methods for this generic function must obey. The contract of the say generic function is as follows: The say generic function receives one required argument, which must be of the type <object>. It prints a description of the object. The say generic function returns no values. Dylan requires all the methods for a generic function to have congruent parameter lists and values declarations. See Parameter-list congruence. Now, we dene two methods for say. The method for say on <time-of-day> fullls the same purpose (and has the same body) as the say-time-of-day method, which we remove from the library with an editor or a gesture in the environment.
define method say (time :: <time-of-day>) => () let (hours, minutes) = decode-total-seconds(time); format-out ("%d:%s%d", hours, if (minutes < 10) "0" else "" end, minutes); end method say;
Similarly, the method for say on <time-offset> is intended to replace say-time-offset, which we remove.
define method say (time :: <time-offset>) => () let (hours, minutes) = decode-total-seconds(time); format-out("%s %d:%s%d", if (past?(time)) "minus" else "plus" end, hours, if (minutes < 10) "0" else "" end, minutes); end method say-time-offset;
The generic function say has two methods dened for it:
define method say (time :: <time-of-day>) => () let (hours, minutes) = decode-total-seconds(time); format-out ("%d:%s%d", hours, if (minutes < 10) "0" else "" end, minutes); end say; define method say (time :: <time-offset>) => () let (hours, minutes) = decode-total-seconds(time); format-out("%s %d:%s%d", if (past?(time)) "minus" else "plus" end, hours, if (minutes < 10) "0" else "" end, minutes); end say;
49
In the preceding call, the argument is of the type <time-of-day>, so the method on <time-of-day> is the only applicable method. That method is invoked.
? say(*my-time-offset*); => plus 15:20
In the preceding call, the argument is of the type <time-offset>, so the method on <time-offset> is the only applicable method. That method is invoked.
In the preceding call, the argument is of the type <time-of-day>, so the method on <time> is the only applicable method. That method is invoked.
? say(*my-time-offset*); => plus 15:20
In the preceding call, the argument is of the type <time-offset>, so two methods are applicable. The method on <time-offset> is more specic than is the method on <time>, so the method on <time-offset> is called. That method on <time-offset> prints minus or plus, and calls next-method. The next-method function calls the method on <time>, which prints the hours and minutes. Using next-method is convenient in cases such as this, where a method on a superclass can do most of the work, but a method on a subclass needs to do additional work. 50 Chapter 3. Part 1. Basic Concepts
When next-method is called with no arguments, as it is in the method on <time-offset>, Dylan calls the next most specic method with the same arguments provided to the method that calls next-method. You can provide arguments to next-method. For example, you could provide a keyword argument with a value that each method can manipulate (such as adding a value to a number, or appending an element to a list). If you provide arguments to next-method, the arguments must be compatible with the generic function, as described in Parameter-list congruence. In addition, you cannot supply required arguments that have classes different from those of the original required arguments to the generic function, if doing so would have changed the method dispatch in any way. Providing arguments to next-method is an advanced technique; see Parameter lists, and Vehicle containers.
The library le denes the time library and the time module. The library le: library.dylan.
module: dylan-user define library time use dylan; use format-out; end library time; define module time use dylan; use format-out; end module time;
The library implementation le denes the classes, methods, and generic functions. The implementation le: library-implementation.dylan.
module: time // Class definitions define class <time> (<object>) slot total-seconds :: <integer>, init-keyword: total-seconds:; end class <time>; // A specific time of day from 00:00 (midnight) to before 24:00 (tomorrow) define class <time-of-day> (<time>) end class <time-of-day>;
51
// A relative time between -24:00 and +24:00 define class <time-offset> (<time>) end class <time-offset>; // Method for determining whether a time offset is in the past define method past? (time :: <time-offset>) => (past? :: <boolean>) time.total-seconds < 0; end method past?; // Methods for encoding and decoding total seconds define method encode-total-seconds (hours :: <integer>, minutes :: <integer>, seconds :: <integer>) => (total-seconds :: <integer>) ((hours * 60) + minutes) * 60 + seconds; end method encode-total-seconds; define method decode-total-seconds (time :: <time>) => (hours :: <integer>, minutes :: <integer>, seconds :: <integer>) decode-total-seconds(abs(time.total-seconds)); end method decode-total-seconds; define method decode-total-seconds (total-seconds :: <integer>) => (hours :: <integer>, minutes :: <integer>, seconds :: <integer>) let (total-minutes, seconds) = truncate/(total-seconds, 60); let (hours, minutes) = truncate/(total-minutes, 60); values(hours, minutes, seconds); end method decode-total-seconds; // The say generic function and its methods // Given an object, print a description of the object define generic say (any-object :: <object>) => (); define method say (time :: <time>) => () let (hours, minutes) = decode-total-seconds(time); format-out ("%d:%s%d", hours, if (minutes < 10) "0" else "" end, minutes); end method say; define method say (time :: <time-offset>) format-out("%s ", if (past?(time)) "minus" else "plus" end); next-method(); end method say;
The test le creates instances and calls say on the instances. The test le can access variables dened in the implementation le, because both les are in the time module. The test le: test.dylan.
module: time define variable *my-time-offset* :: <time-offset> = make(<time-offset>, total-seconds: encode-total-seconds(15, 20, 10)); define variable *your-time-offset* :: <time-offset>
52
= make(<time-offset>, total-seconds: - encode-total-seconds(6, 45, 30)); define variable *my-time-of-day* = make(<time-of-day>, total-seconds: encode-total-seconds(0, 2, 0)); define variable *your-time-of-day* = make(<time-of-day>, total-seconds: encode-total-seconds(8, 30, 59)); say(*my-time-offset*); say(*your-time-offset*); say(*my-time-of-day*); say(*your-time-of-day*);
When we run the test.dylan le, Dylan creates two instances of <time-offset> and two instances of <time-of-day>. It calls say on all four instances. The output of the test is
plus 15:20 minus 6:45 0:02 8:30
3.5.9 Summary
In this chapter, we covered the following: We showed how to use class inheritance. We introduced the terminology of classes: direct subclass, subclass, direct superclass, superclass, direct instance, indirect instance, instance, subtype, and supertype. We showed how method dispatch works for a generic function with one argument, when there is more than one applicable method. We created a generic function explicitly (with define generic). We used next-method to call the next most specic method.
3.6 Multimethods
In this chapter, we show two important techniques. First, we dene methods for built-in generic functions in this case, for the functions +, <, and =. Second, we dene multimethods. We describe how method dispatch works for multimethods.
3.6. Multimethods
53
Comparison with C++ and Java: In C++, operator overloading means customizing the action of any built-in operator for classes that you dene. In Dylan, operators are just generic functions, and you can add methods to those generic functions for your classes. In C++, the meaning of an overloaded operator is resolved at compile time the types of the operands must be known at compile time. Because Dylan operators are generic functions, the method is chosen dynamically according to the argument types at run time, if the types may vary at run time. Java does not allow operator overloading. The Java designers believe that overloading of operators results in inscrutable code (because the meaning of the operator can vary). Dylan and C++ designers believe that, judiciously used, operator overloading permits clearer, more concise code.
Method for adding two time offsets We now dene a method for +. The method adds two time offsets and returns the sum, which is also a time offset:
1 2 3 4 5 6 7
// Method on <time-offset>, <time-offset> define method \+ (offset1 :: <time-offset>, offset2 :: <time-offset>) => (sum :: <time-offset>) let sum = offset1.total-seconds + offset2.total-seconds; make(<time-offset>, total-seconds: sum); end method \+;
On line 2, notice that the method is dened on \+, rather than simply on +. When we dene a method on + or on another inx function, we need to use a backslash before the function name. The backslash claries that we mean the value of the variable + (which is a generic function), and that we are not trying to call the function. On line 4, we add the values stored in the total-seconds slots of the two instances. On line 5, we make and return a new instance of <time-offset>. We initialize the total-seconds slot to contain the sum calculated in line 4. To test the method, we need to create two instances of <time-offset>:
define variable *minus-2-hours* = make(<time-offset>, total-seconds: - encode-total-seconds (2, 0, 0)); define variable *plus-15-20-45* = make(<time-offset>, total-seconds: encode-total-seconds (15, 20, 45));
The result is a new instance of <time-offset>. We did not save the value returned. (Many environments offer a way to access values returned by the listener.) We can add the time offsets again, and view the total-seconds slot of the result:
? decode-total-seconds(*minus-2-hours* + *plus-15-20-45*); => 13 => 20 => 45
Methods for adding a time of day to a time offset These methods implement addition between a time offset and a time of day: 54 Chapter 3. Part 1. Basic Concepts
// Method on <time-offset>, <time-of-day> define method \+ (offset :: <time-offset>, time-of-day :: <time-of-day>) => (sum :: <time-of-day>) make(<time-of-day>, total-seconds: offset.total-seconds + time-of-day.total-seconds); end method \+;
The method on <time-offset>, <time-of-day> is invoked when the rst argument is a time offset and the second argument is a time of day. It does the work of creating a new <time-of-day> instance with the total-seconds slot initialized to the sum of the total-seconds slots of the two arguments.
// Method on <time-of-day>, <time-offset> define method \+ (time-of-day :: <time-of-day>, offset :: <time-offset>) => (sum :: <time-of-day>) offset + time-of-day; end method \+;
The method on <time-of-day>, <time-offset> is invoked when the rst argument is a time of day and the second argument is a time offset. It simply calls + with the order of the arguments switched this call invokes the method on <time-offset>, <time-of-day>. To test these methods, we can use one of the time offsets created in Method for adding two time offsets, and dene the *8-30-59* variable, which contains a <time-of-day> instance, which we dene as follows:
define variable *8-30-59* = make(<time-of-day>, total-seconds: encode-total-seconds(8, 30, 59));
Method for adding other kinds of times We have already dened methods for adding the kinds of time that it makes sense to add together. It is not logical to add one time of day to another time of day what would three oclock plus two oclock mean? Someone could create another concrete subclass of <time>, without providing any methods for adding that time to other times. If someone tries to add times that we do not intend them to add, the result will be a No applicable method error. We could provide a method whose sole purpose is to give more information to the user than No applicable method when + is called on two times that cannot be added, because there is no applicable method for adding them. We dene such a method here:
// Method on <time>, <time> define method \+ (time1 :: <time>, time2 :: <time>) error("Sorry, we cant add a %s to a %s.", object-class(time1), object-class(time2)); end method \+;
3.6. Multimethods
55
This method is called only when the arguments are both general instances of <time>, and none of the more specic methods are applicable to the arguments. The error function signals an error. For more information about signaling and handling errors, see Exceptions. Note: This method is useful for explaining how method dispatch works for multimethods, but it does not really give the user any more useful information than that supplied by the No applicable method error. Therefore, we dene the method in this chapter, but do not include it as part of the nal library.
Applicable methods for different arguments to +, ordered by specicity. shows the applicable methods for various arguments to +. If two methods are applicable, we number the more specic method 1, and the less specic method 2. We call + on two instances of <time-offset>:
? *minus-2-hours* + *plus-15-20-45*; => {instance of <time-offset>}
56
When both arguments are instances of <time-offset>, the rst row of the table applies. Two methods are applicable. The method on <time-offset>, <time-offset> is more specic than the method on <time>, <time>. The parameter specializers of the method on <time-offset>, <time-offset> are subtypes of the parameter specializers of the method on <time>, <time>. That is, for the rst parameter, <time-offset> is a subtype of <time>; for the second parameter, <time-offset> is a subtype of <time>.
To compare times, we need only to dene methods for < and =. All other numerical comparisons in Dylan are based on these two methods. So, we can call >, >=, <=, and ~= (the not-equal-to function). Here are examples:
? *plus-15-20-45* ~= *minus-2-hours*; => #t ? *plus-15-20-45* > *minus-2-hours*; => #t
3.6.4 Summary
In this chapter, we covered the following: We dened new methods on the built-in generic functions +, <, and =. We discussed how method dispatch works for multimethods.
3.7 Modularity
Object-oriented programming can lead to modular code. When you are experienced with an object-oriented programming style, you might be able to dene classes and methods with the right modularity from the start. Novices, however and even experienced object-oriented programmers who are attacking large problems may nd that they discover opportunities for sharing as they begin to implement classes and methods. The dynamic aspects of Dylan support an 3.7. Modularity 57
evolutionary approach to programming, so it is easy to continue to rene your implementation and to design as you go. In this chapter, we show an evolutionary approach to programming, as we dene classes that represent different kinds of positions. We start out with one approach, and gradually rene it to achieve greater modularity. We illustrate one new Dylan feature: abstract classes. Starting in this chapter, and continuing throughout the rest of the book, we take the approach of editing and compiling source code. Now and then, we use a listener to call a function and show the functions output. Whenever we use a listener, we show the ? prompt.
These initial denitions show the inheritance relationships among the classes, and the names of the slots show the information that the classes must provide. At this point, we omit the type declarations of the slots, which is equivalent to specifying the type <object>. We will ll in the implementation later, by deciding on the types of the slots, and providing the say methods. 58 Chapter 3. Part 1. Basic Concepts
Our requirements mention only <absolute-position> and <relative-position>, but we choose to dene a superclass of both of them, named <position>. Modularity note: The benets of dening the <position> class are these: The <position> class creates an explicit relationship between the other position classes, which are related conceptually. We can use the <position> class as the type of a slot or other object, in cases where either an absolute or relative position is appropriate.
The <time> class is another one that we intend to have no direct instances, so we redene it to be abstract:
define abstract class <time> (<object>) slot total-seconds :: <integer>, init-keyword: total-seconds:; end class <time>;
If we tried to make an instance of <position> or <time> now, make would signal an error. For more information about abstract classes, see Abstract, concrete, and instantiable classes.
3.7. Modularity
59
Modularity note: The <directed-angle> class represents the characteristics that latitude and longitude have in common.
Comparison to C: If you are familiar with a language that uses explicit pointers, such as C, you may be confused by Dylans object model. Although there is no pointer-to operation in Dylan, there are pointers in the implementation. If you are trying to imagine how Dylan objects are implemented, think in terms of always manipulating a pointer to the object a Dylan variable (or slot) stores a pointer to an object, rather than a copy of the objects slots. Similarly, assignment, argument passing, and identity comparison are in terms of pointers to objects. See Dylan Object Model for C and C++ Programmers.
Comparison to Java: Java recognizes that pointers make it extremely difcult to enforce safety and for a compiler to reason about a program for optimization. Java supports an object model similar to that of Dylan, where pointers are used in the implementation of objects, but are not visible to Java programs. We could dene the say method as follows:
define method say (position :: <absolute-position>) => () format-out("%d degrees %d minutes %d seconds %s latitude\n", decode-total-seconds(position.latitude)); format-out("%d degrees %d minutes %d seconds %s longitude\n", decode-total-seconds(position.longitude)); end method say;
The preceding method depends on decode-total-seconds having a method that is applicable to <directed-angle> (the type of the objects returned by position.latitude and position.longtude). We dene such a method in Meeting of angles and times. Modularity note: The preceding say method does not take advantage of the similarity between latitude and longitude. One clue that there is a modularity problem is that the two calls to format-out are nearly identical. The say method on <absolute-position> should not call format-out directly on the two instances of <directed-angle> stored in the latitude and longitude slots. Instead, we can dene a say method on <directed-angle>, and can call it in the method on <absolute-position>:
define method say (angle :: <directed-angle>) => () let (degrees, minutes, seconds) = decode-total-seconds(angle); format-out("%d degrees %d minutes %d seconds %s", degrees, minutes, seconds, angle.direction); end method say; define method say (position :: <absolute-position>) => () say(position.latitude); format-out(" latitude\n"); say(position.longitude); format-out(" longitude\n");
60
Modularity note: Our modularity is improved, now that the <directed-angle> class is responsible for describing its instances. This division of labor reduces duplication of code. There is still a problem with this approach, because the say method on <absolute-position> must print latitude and longitude after calling say on the directed angles stored in its two slots. The modularity is still awed, because the method on <absolute-position> acts on the knowledge that the method on <directed-angle> does not print latitude or longitude. We dened the <directed-angle> class to represent what latitude and longitude have in common. It is useful to recognize that latitude and longitude have differences as well as similarities. We represented latitude and longitude by the names of slots in <absolute-position>, and their implementations as instances of <directed-angle>. We can elevate the visibility of latitude and longitude by providing classes that represent each of them:
define class <latitude> (<directed-angle>) end class <latitude>; define class <longitude> (<directed-angle>) end class <longitude>;
Figure 3.6: Inheritance relationships among the position and angle classes. Abstract classes are shown in oblique typewriter font. Inheritance relationships among the position and angle classes shows the inheritance relationships among the position and angle classes. We dene these new say methods:
define method say (latitude :: <latitude>) => () next-method(); format-out(" latitude\n"); end method say; define method say (longitude :: <longitude>) => () next-method(); format-out(" longitude\n"); end method say;
3.7. Modularity
61
The calls to next-method in the methods on <latitude> and <longitude> will call the method on <directed-angle>, shown previously. We redene the say method on <absolute-position>:
define method say (position :: <absolute-position>) => () say(position.latitude); say(position.longitude); end method say;
Modularity note: The approach of dening the classes <latitude> and <longitude> provides the following benets: Each class is responsible for describing its instances. Each method depends on say working for all the classes. No method on one class must understand the details of a method on another class. We guard against any attempt to store a latitude in a slot designated for a longitude, and vice versa. This type checking will be useful when we introduce more differences between the classes. For example, the direction of a latitude is north or south, and the direction of a longitude is west or east. We can provide methods that ensure that the directions stored in a <latitude> instance are appropriate for latitude and we can do the same for longitude. We show two techniques for implementing that type checking: See Virtual slots, and Enumerations. You can ask an object what its class is by using the object-class function. In this case, you can nd out that an object is a latitude or longitude, rather than just a directed angle. The data does not stand alone; it is an instance that carries with it its type, its identity, and the methods appropriate to it.
The distance slot stores the distance to the other object, and the angle slot stores the direction to the other object. Unfortunately, the angle needed here is different from the <directed-angle> class, because the <directed-angle> class has a direction, such as south, which is not needed for the angle of <relative-position>. We need to provide a class of angle without direction, which we can use for the angle slot of the <relative-position> class). Therefore, we dene two new classes, and redene <directed-angle>:
// Superclass of all angle classes define abstract class <angle> (<object>) slot total-seconds :: <integer>, init-keyword: total-seconds:; end class <angle>; define class <relative-angle> (<angle>) end class <relative-angle>; define abstract class <directed-angle> (<angle>) slot direction :: <string>, init-keyword: direction:; end class <directed-angle>;
62
Modularity note: Why provide both the classes <angle> and <relative-angle>, when the <relative-angle> class has no additional slots? We need a class that has only the total-seconds slot, and no others. We need to use such a class as the type of the angle slot of <relative-angle>. We might consider making the <angle> class concrete, and using that class, which has only the total-seconds slot. However, that approach would not prevent someone from storing a <directed-angle> instance in the angle slot of <relative-angle>, because <directed-angle> instances are also instances of <angle>. In Dylan, by dening classes as specically as possible, you enhance the reliability of your program, because the compiler (or run-time system) can verify that only correct values are used. In contrast, you could write a program in Dylan or C in which you represented everything as an integer in that style of program, someone could far too easily introduce a programming error in which a time was stored where a latitude was needed. The <angle> class looks remarkably similar to the <time> class dened earlier:
// Superclass of all angle classes define abstract class <angle> (<object>) slot total-seconds :: <integer>, init-keyword: total-seconds:; end class <angle>; // Superclass of all time classes define abstract class <time> (<object>) slot total-seconds :: <integer>, init-keyword: total-seconds:; end class <time>;
We would like to call decode-total-seconds on instances of <angle>, but currently the method is dened to work on <time>. The next step is to take advantage of the similarity between <angle> and <time>.
We redene the time and angle classes and methods to take advantage of the new <sixty-unit> class:
3.7. Modularity
63
define abstract class <time> (<sixty-unit>) end class <time>; define abstract class <angle> (<sixty-unit>) end class <angle>; define method say (angle :: <angle>) => () let (degrees, minutes, seconds) = decode-total-seconds(angle); format-out("%d degrees %d minutes %d seconds", degrees, minutes, seconds); end method say; // definition unchanged, repeated for completeness define abstract class <directed-angle> (<angle>) slot direction :: <string>, init-keyword: direction:; end class <directed-angle>; define method say (angle :: <directed-angle>) => () next-method(); format-out(" %s", angle.direction); end method say; // definition unchanged, repeated for completeness define class <relative-angle> (<angle>) end class <relative-angle>; // we need to show degrees for <relative-angle>, but do not need to show // minutes and seconds,so we override the method on <angle> define method say (angle :: <relative-angle>) => () format-out(" %d degrees", decode-total-seconds(angle)); end method say; define method say (position :: <relative-position>) => () format-out("%d miles away at heading ", position.distance); say(position.angle); end method say;
To see the complete library, and the test code that creates position instances and calls say on them, see A Simple Library. Is-a relationships (inheritance) among classes shows the inheritance relationships of the classes. When one class inherits from another, the relationship is sometimes called the is-a relationship. For example, a direct instance of <time-offset> is a <time> as well, and it is a <sixty-unit>. The classes have another kind of relationship as well one class can use another class as the type of a slot, in what is called the has-a relationship. Has-a relationships among classes shows both the inheritance relationships, and the relationships of one class using another class as the type of a slot.
64
Figure 3.7: Is-a relationships (inheritance) among classes, shown by arrows. Abstract classes are shown in oblique typewriter font.
3.7. Modularity
65
Instantiable classes A class that can be used as the rst argument to make is an instantiable class. All concrete classes are instantiable. When you dene an abstract class, Dylan does not provide a method for make that enables you to create direct instances of that class. Thus, if you call make on an abstract class, you get an error. Even though an abstract class does not have direct instances, it is sometimes possible to use an abstract class as the rst argument to make. In this case, the make function creates and returns a direct instance of a concrete subclass of the abstract class. In other words, make can return either a direct or an indirect instance of its rst argument. To make it possible for an abstract class to be provided as the rst argument to make, you dene the abstract class, and dene one or more concrete subclasses of it. You then dene a method for make that specializes its rst parameter on the abstract class, and that returns an instance of one of its concrete subclasses. To dene make methods, you need to use the singleton function to create a type whose only instance is the class itself; see Nonclass Types. Denition of make methods is an advanced topic that we do not cover in this book. What is the reason for enabling users to call make on an abstract class? This exibility allows a program that needs a general kind of object, represented by a superclass, to ask for an instance of the superclass without specifying the direct class of the instance. For example, a program might need to store data in a vector, but might not be concerned about the specic implementation of the vector that it uses. Such a program can create a vector by calling make with the argument <vector>, and make will create an instance of a concrete subclass. The built-in <vector> class is abstract, but is instantiable. Design considerations for abstract classes The built-in Dylan classes follow a design principle in which concrete classes do not inherit from other concrete classes, but rather inherit from abstract classes only. In other words, the branches of the tree are abstract classes, and the leaves of the tree are concrete classes. We follow that design principle in this book as well. Is-a relationships (inheritance) among classes shows our classes graphically; the branches of the tree (abstract classes) appear in oblique typewriter font, and the leaves (concrete classes) appear in bold typewriter font. Abstract classes can ll two roles. First, they act as an interface. For example, the <sixty-unit> class is an interface. If an object is of the <sixty-unit> type, you can expect certain behaviors from that object. Those behaviors are the generic functions that are specialized on <sixty-unit>, including decode-total-seconds, and total-seconds. Abstract classes can also act as a partial implementation, if they dene slots. The slots in an abstract class are useful for the classes that inherit from that class. For example, the <sixty-unit> class denes the total-seconds slot, which is useful for <time> and <position>.
3.7.8 Summary
In this chapter, we covered the following: A class can represent characteristics and behavior in common across other classes. For example, the <directed-angle> class represents the degrees-minutes-seconds aspects that are common to latitude and longitude. Also, the <sixty-unit> class represents the total-seconds that are common to <time> and <angle>. Classes can be used to represent differences between two similar kinds of objects. For example, the <latitude> and <longitude> classes are similar in that both classes inherit from <directed-angle>, and neither class denes additional slots. However, by providing the two classes, <latitude> and <longitude>, we make it possible to identify objects as being of type <latitude> or <longitude>, and we make it possible to customize the behavior of operations on <latitude> and <longitude> as needed.
66
In many object-oriented libraries and programs, certain classes are not intended to have direct instances. You can dene those classes as abstract classes to document their purpose. When you have two related classes and both will have direct instances, it is good practice to dene a third class to be the superclass of the two other classes. The superclass is abstract, and the other two classes are concrete. We used this style in the time classes, the angle classes, and the position classes. People can use the abstract superclasses, such as <position>, as the type of objects that can be any kind of position. In proper modularity, a method on a particular class should not depend on information that is private to second class. If someone changes the representation of the second class, the method could break. We showed an example of breaking this rule when one version of the say method on <absolute-position> printed latitude and longitude after calling say on the directed angles stored in its two slots. The method on <absolute-position> acted on the knowledge that the method on <directed-angle> does not print latitude or longitude. One of the challenges of modular design is for you to decide which attributes to generalize (by moving them up to higher, or more general, classes in the inheritance graph), and which attributes to specialize (by moving them down the inheritance graph into more specic classes). Another challenge is deciding when to split a class into multiple behaviors, and when to introduce more abstract classes to hold shared behavior. No computer language can make these decisions for you, but dynamic languages typically allow more freedom to explore these relationships. Generic functions and multimethods allow more freedom in dening behavior than does attaching a method to a single class.
67
68
// The time classes and methods define abstract class <time> (<sixty-unit>) end class <time>; define method say (time :: <time>) => () let (hours, minutes) = decode-total-seconds(time); format-out ("%d:%s%d", hours, if (minutes < 10) "0" else "" end, minutes); end method say; // A specific time of day from 00:00 (midnight) to before 24:00 (tomorrow) define class <time-of-day> (<time>) end class <time-of-day>; // A relative time between -24:00 and +24:00 define class <time-offset> (<time>) end class <time-offset>; // Method for determining whether a time offset is in the past define method past? (time :: <time-offset>) => (past? :: <boolean>) time.total-seconds < 0; end method past?; define method say (time :: <time-offset>) format-out("%s ", if (past?(time)) "minus" else "plus" end); next-method(); end method say; // Methods for adding times define method \+ (offset1 :: <time-offset>, offset2 :: <time-offset>) => (sum :: <time-offset>) let sum = offset1.total-seconds + offset2.total-seconds; make(<time-offset>, total-seconds: sum); end method \+; define method \+ (offset :: <time-offset>, time-of-day :: <time-of-day>) => (sum :: <time-of-day>) make(<time-of-day>, total-seconds: offset.total-seconds + time-of-day.total-seconds); end method \+; define method \+ (time-of-day :: <time-of-day>, offset :: <time-offset>) => (sum :: <time-of-day>) offset + time-of-day; end method \+; // Methods for comparing times define method \< (time1 :: <time-of-day>, time2 :: <time-of-day>) time1.total-seconds < time2.total-seconds; end method \<; define method \< (time1 :: <time-offset>, time2 :: <time-offset>)
69
time1.total-seconds < time2.total-seconds; end method \<; define method \= (time1 :: <time-of-day>, time2 :: <time-of-day>) time1.total-seconds = time2.total-seconds; end method \=; define method \= (time1 :: <time-offset>, time2 :: <time-offset>) time1.total-seconds = time2.total-seconds; end method \=; // The angle classes and methods define abstract class <angle> (<sixty-unit>) end class <angle>; define method say (angle :: <angle>) => () let (degrees, minutes, seconds) = decode-total-seconds(angle); format-out ("%d degrees %d minutes %d seconds", degrees, minutes, seconds); end method say; define class <relative-angle> (<angle>) end class <relative-angle>; // We need to show degrees for <relative-angle> but we do not need to // show minutes and seconds, so we override the method on <angle> define method say (angle :: <relative-angle>) => () format-out(" %d degrees", decode-total-seconds(angle)); end method say; define abstract class <directed-angle> (<angle>) slot direction :: <string>, init-keyword: direction:; end class <directed-angle>; define method say (angle :: <directed-angle>) => () next-method(); format-out(" %s", angle.direction); end method say; // The latitude and longitude classes and methods define class <latitude> (<directed-angle>) end class <latitude>; define method say (latitude :: <latitude>) => () next-method(); format-out(" latitude\n"); end method say; define class <longitude> (<directed-angle>) end class <longitude>; define method say (longitude :: <longitude>) => () next-method(); format-out(" longitude\n"); end method say;
70
// The position classes and methods define abstract class <position> (<object>) end class <position>; define class <absolute-position> (<position>) slot latitude :: <latitude>, init-keyword: latitude:; slot longitude :: <longitude>, init-keyword: longitude:; end class <absolute-position>; define method say (position :: <absolute-position>) => () say(position.latitude); say(position.longitude); end method say; define class <relative-position> (<position>) // Distance is in miles slot distance :: <single-float>, init-keyword: distance:; slot angle :: <angle>, init-keyword: angle:; end class <relative-position>; define method say (position :: <relative-position>) => () format-out("%d miles away at heading ", position.distance); say(position.angle); end method say;
71
format-out("\n"); format-out("Creating an instance of <time-offset> in *minus-2-hours*.\n"); define variable *minus-2-hours* = make(<time-offset>, total-seconds: - encode-total-seconds (2, 0, 0)); format-out("Creating an instance of <time-offset> in *plus-15-20-45*.\n"); define variable *plus-15-20-45* = make(<time-offset>, total-seconds: encode-total-seconds (15, 20, 45)); format-out("Creating an instance of <time-of-day> in *8-30-59*.\n"); define variable *8-30-59* = make(<time-of-day>, total-seconds: encode-total-seconds (8, 30, 59)); format-out("Adding <time-offset> + <time-offset>: *minus-2-hours* + *plus-15-20-45*:\n"); decode-total-seconds(*minus-2-hours* + *plus-15-20-45*); format-out("Adding <time-offset> + <time-of-day>: *minus-2-hours* + *8-30-59*:\n"); decode-total-seconds(*minus-2-hours* + *8-30-59*); format-out("Adding <time-of-day> + <time-offset>: *8-30-59* + *minus-2-hours*:\n"); decode-total-seconds(*8-30-59* + *minus-2-hours*);
When we run the test le, we see the following output and values:
Creating an instance of <absolute-position>: 42 degrees 19 minutes 34 seconds North latitude 70 degrees 56 minutes 26 seconds West longitude* Creating an instance of <relative-position>: 30 miles away at heading 90 degrees* Creating an instance Creating an instance Creating an instance Adding <time-offset> *plus-15-20-45": 13 20 45 Adding <time-offset> 6 30 59 Adding <time-of-day> 6 30 59 of <time-offset> of <time-offset> of <time-of-day> + <time-offset>: in *minus-2-hours*. in *plus-15-20-45*. in *8-30-59*. *minus-2-hours* +
72
3.8.5 Summary
In this chapter, we created the four les that constitute the timespace library. Introduction, describes the goals of Dylan, and tells you where Dylan ts in the world of programming languages. Quick Start, is a practical guide for getting started using Dylan. It shows the look and feel of a hypothetical Dylan listener, introduces the most basic concepts of Dylan, and presents a complete Dylan program. You can type in these examples and experiment with Dylan. Methods, Classes, and Objects, introduces the concepts of methods, built-in classes, class inheritance, and explains what it means to be an object. In Chapters User-Dened Classes and Methods through Modularity, we start to develop an example of a library that represents different kinds of time and position. A library is a complete unit of code that can be used by many different clients. Our eventual goal in this book is to develop a sample application that handles the scheduling of aircraft that are arriving at, and departing from, an airport. For more information, see Design of the Airport Application. The airport application will use the time and position library. Also in Chapters User-Dened Classes and Methods through Modularity, we show how to write object-oriented programs in Dylan. We explain class and method denition, class inheritance, method dispatch, and modularity. A Simple Library contains the code developed in Part 1. Basic Concepts as a complete working library.
73
74
CHAPTER
FOUR
We can use a nonclass type as a parameter specializer of a method, or as the type of a return value:
define method encode-total-seconds (max-unit :: <nonnegative-integer>, minutes :: <nonnegative-integer>,
75
seconds :: <nonnegative-integer>) => (total-seconds :: <nonnegative-integer>) ((max-unit * 60) + minutes) * 60 + seconds; end method encode-total-seconds;
To see how we use <nonnegative-integer> in the time library, see Setter methods. We can dene a type whose only member is the false value, #f:
singleton(#f);
We can dene a type that is the union of the false value and <integer>:
type-union(singleton(#f), <integer>);
We can make it convenient for people to create new types like the one dened in the preceding code. The new type is the union of the false value and the argument to the method:
define method false-or (other-type :: <type>) => (combined-type :: <type>) type-union(singleton(#f), other-type); end method false-or;
false-or types are useful as the type of slots. Note that a slot can be uninitialized. Once a slot receives a value, however, it will always have a value: There is no way to return a slot to the uninitialized state. Sometimes it is useful to store in a slot a value that means none. Later on in our development of the airport example, we use a false-or type as the type of a slot that stores the next vehicle, if there is one. If there is no next vehicle, the slot contains #f. We create the type by calling false-or(<vehicle>), and use the result as the type of the slot. Note that, if the type of the slot were just <vehicle>, we could not store #f in the slot, and there would be no way to represent none. You can use type-union and singleton together to dene a type that is an enumeration of multiple-choice objects. For example,
define constant <latitude-direction> = type-union(singleton(#"north"), singleton(#"south"));
The <latitude-direction> type has two valid values: the keywords #"north" and #"south". For an explanation of how we could use that type to enforce the correct values of a latitude slot, and for information about the performance of enumerations, see Enumerations.
76
(In the presence of multiple inheritance, the specicity rule is more complex. For more information, see Multiple inheritance and method dispatch.) 3. Call the most specic method. (If there is more than one required argument, Dylan constructs the sorted list of methods by combining separate sorted lists for all required arguments.) For any given argument and any given set of parameter types, Dylan has to answer two questions: 1. Is the argument an instance of a given type? The answer determines method applicability. 2. Is one type a proper subtype of another type? The answer determines method specicity. Method dispatch and classes We have already seen that, when all types are classes, Dylan uses the following rules: 1. An object is an instance of a class if it is a general instance of that class (a direct instance of the class or of one of that classs subclasses). 2. One class is a proper subtype of another if the rst class is a subclass of the second. For example, suppose that we have these denitions:
// Method 1 define method say (x :: <number>) ... end method say; // Method 2 define method say (x :: <integer>) ... end method say;
Now, if say is called with an argument of 100, both methods are applicable, and method 2 is more specic than method 1. Method dispatch and singletons When a type is a singleton, Dylan uses the following rules: 1. An object is an instance of a singleton only if the object is identical to the object used as the argument in the call to singleton that created the singleton. 2. A singleton is a proper subtype of any other type that the object belongs to. Thus, a singleton is more specic than any other type of which an object is an instance. In particular, a singleton is more specic than the objects class. For example, suppose that we have these denitions:
// Method 1 define method say (x :: <integer>) ... end method say; // Method 2 define method say (x == 0) ... end method say;
Note that method 2 illustrates a convenient syntax for dening a method on a singleton without calling singleton explicitly. Now, if say is called with an argument of 0, both methods are applicable, and method 2 is more specic than method 1. If say is called with an argument that is any other integer, only method 1 is applicable.
77
Method dispatch and unions When a type is a union, Dylan uses the following rules: 1. An object is an instance of a union if it is an instance of any of the types that make up that union. 2. If none of the types that make up a union is a subtype of any other, then 3. A nonunion type is a proper subtype of a union if the nonunion type is a subtype of any of the types that make up the union. 4. A union is a proper subtype of a nonunion type if all types that make up the union are subtypes of the nonunion type, and if all the types that make up the union, taken together, are not equivalent to the nonunion type. 5. A union is a proper subtype of another union if each of the types that make up the rst union is a subtype of one of the types that make up the other union, and if the two unions are not equivalent. For example, suppose that we have these denitions:
define constant <false-or-integer> = type-union(<integer>, singleton(#f)); // Method 1 define method say (x :: <false-or-integer>) ... end method say; // Method 2 define method say (x :: <integer>) ... end method say;
Now, if say is called with an argument that is an integer, both methods are applicable, and method 2 is more specic than method 1. If say is called with an argument of #f, only method 1 is applicable. Method dispatch and limited integers When a type is a limited-integer type, Dylan uses the following rules: 1. An object is an instance of a limited-integer type if it is an instance of <integer> and if it is (inclusively) within the specied range. 2. A limited-integer type is a proper subtype of <integer>, as long as it is not equivalent to <integer>. One limited-integer type is a proper subtype of another limited-integer type if the range of the rst type is entirely within the range of the second type, and if the two types are not equivalent. For example, suppose that we have these denitions:
define constant <nonnegative-integer> = limited(<integer>, min: 0); // Method 1 define method say (x :: <integer>) ... end method say; // Method 2 define method say (x :: <nonnegative-integer>) ... end method say;
Now, if say is called with an argument of 1, both methods are applicable, and method 2 is more specic than method 1. If say is called with an argument of -1, only method 1 is applicable. Now suppose that, instead, we have the following denitions:
define constant <limited-integer-1> = limited(<integer>, min: -2, max: 2);
78
define constant <limited-integer-2> = limited(<integer>, min: 0, max: 4); // Method 1 define method say (x :: <limited-integer-1>) ... end method say; // Method 2 define method say (x :: <limited-integer-2>) ... end method say;
Now, if say is called with an argument of 1, both methods are applicable, and neither method is more specic than the other; the two methods are ambiguous. If no more specic method exists, Dylan signals an error when we call say with an argument of 1. Method dispatch and limited collections When a type is a limited-collection type, Dylan uses the following rules: 1. An object is an instance of a limited-collection type if all the following are true: the class of the object is a subclass of the base type; the two element types are equivalent; and, if the limited-collection type restricts the size or dimensions, the size or dimensions of the object are the same as those specied for the type. If the object is an instance of <strectchy-collection>, the limited-collection type cannot restrict the size or dimensions. 2. A limited-collection type is a proper subtype of its base type, as long as it is not equivalent to the base type. Generally, one limited-collection type is a proper subtype of another limited-collection type if all the following are true: the base type of the rst is a subclass of the base type of the second; the two element types are equivalent; the size or dimensions of the rst limited type are no less restricted than those of the second type; and the rst limited type is not equivalent to the second. For example, suppose that we have these denitions:
define constant <limited-vector-of-3-integers> = limited(<vector>, of: <integer>, size: 3); define constant <limited-vector-of-3-numbers> = limited(<vector>, of: <number>, size: 3); define constant $v1 = make(<limited-vector-of-3-integers>, size: 3, fill: 1); define constant $v2 = vector(1, 1, 1); // Method 1 define method say (x :: <vector>) ... end method say; // Method 2 define method say (x :: <limited-vector-of-3-integers>) ... end method say; // Method 3 define method say (x :: <limited-vector-of-3-numbers>) ... end method say;
Now, if say is called with an argument of $v1, both method 1 and method 2 are applicable, and method 2 is more specic than method 1. Note that $v1 is an instance of <limited-vector-of-3-integers> but is not an in-
79
stance of <limited-vector-of-3-numbers>, because the element type of $v1 is not equivalent to the element type of <limited-vector-of-3-numbers>. If say is called with an argument of $v2, only method 1 is applicable. Note that $v2 is not an instance of either of the limited-collection types we dened, even though $v2 is a vector that contains three integers. (For example, we could store objects other than integers in $v2.)
4.1.4 Summary
In this chapter, we discussed types that are not classes: A singleton type is a type whose only member is one particular instance. An example of creating a singleton type is:
singleton(#f);
A union type is a type whose members include all the members of one or more base types. An example of creating a union type is:
type-union(singleton(#f), <integer>);
A limited type is a type that is a more restricted version of its base type. For example, a limited-integer type is based on <integer>, but has a given minimum or maximum value:
limited(<integer>, min: 0);
Another example of a limited type is a limited-collection type, which is a collection type that species the type of elements, and/or the size of the collection:
limited(<vector>, of: <integer>, size: 3);
4.2 Slots
In this chapter, we show how to call getters and setters with the function-call syntax, and how to dene methods for getters and setters. We show techniques for initializing slots, including slot options and initialize methods. We describe the different allocations that slots can have. We nd a need for symbols, so we describe and use symbols as well.
The dot syntax used with the assignment operator also is an abbreviation for a function call. The rst two expressions are abbreviations for the third expression:
object.name := new-value; name(object) := new-value; name-setter(new-value, object);
You can use the dot syntax as an abbreviation for any function call that takes a single argument and returns a single value. For example, in Methods on <time-offset>, we dened the following method:
80
define method past? (time :: <time-offset>) => (past? :: <boolean>) time.total-seconds < 0; end method past?;
In the remainder of this book, we use the dot syntax for function calls that return a property of an object (such as the past? property of a <time-offset> instance), and that take a single argument and return a single value.
The preceding expressions are calls to the getter function named total-seconds. The choice of which syntax to use is purely a matter of personal style. The rst syntax is provided for those people who prefer the slightly more concise dot syntax. The second syntax is provided for those people who prefer slot accesses to look like function calls. In this book, we use the dot syntax. By default, the name of the setter is the slots name followed by -setter. Thus, the setter for the total-seconds slot is total-seconds-setter. You can use the setter: slot option to specify a different name for the setter. The dot-syntax abbreviation for assignment enables you to invoke the setter by using assignment with the name of the getter. For example, the rst two expressions are abbreviations for the third expression:
*my-time-of-day*.total-seconds := 180; total-seconds(*my-time-of-day*) := 180; total-seconds-setter(180, *my-time-of-day*);
Each of these expressions stores the value 180 in the slot named total-seconds of the object that is the value of the *my-time-of-day* variable. Most Dylan programmers do not use the syntax of the third expression to call a setter, because it is more verbose than the rst and second expressions. However, it is important to know the name of the setter, so that you can dene setter methods. For example, to dene a method on the setter for the total-seconds slot, you dene it on total-seconds-setter. For an example of a setter method, see Setter methods. If you do not want Dylan to dene a setter method for a slot, you can dene the slot to be constant, using the constant slot adjective, or you can give the setter: #f slot option. For more information about accessing slots, see Slot references, and Assignment.
4.2. Slots
81
Advantages of accessing slots via generic functions A slot is conceptually like a variable, in that it has a value. But the only way to access a slots value is to call a generic function. Using generic functions and methods to gain access to slot values has three important advantages: Generic functions provide a public interface to the private implementation of a slot. By making the representation of the slot visible to only the methods of the generic functions, you can change the representation without changing any of the users of the information the callers of the generic functions. In most cases, a compiler can optimize slot references to reduce or eliminate the cost of hiding the implementation. A subclass can specialize, or lter, references to superclass slots. For example, the classes <latitude> and <longitude> inherit the direction slot from their superclass <directed-angle>. In Virtual slots, we show how to provide a setter method for the direction slot of <latitude> that ensures that the value is north or south, and a setter method for the direction slot of <longitude> that ensures that the value is east or west. A slot access can involve arbitrary computation. For example, a slot can be virtual. See Virtual slots. Setter methods In most cases, the getter and setter methods that Dylan denes for each slot are perfectly adequate. In certain cases, however, you might want to change the way a getter or setter works. For example, we can dene a setter method to solve a problem in our time library. The class <time-of-day> inherits the total-seconds slot from the class <sixty-unit>. The type of the slot is <integer> . However, the semantics of <time-of-day> state that the total-seconds should not be less than 0. We can dene a setter method for <time-of-day> to ensure that the new value for the total-seconds slot is 0 or greater. In our setter method, we will use the type dened in Examples of types that are not classes, and repeated here:
// Define nonnegative integers as integers that are >= zero define constant <nonnegative-integer> = limited(<integer>, min: 0);
When the setter for the total-seconds slot is called with an instance of <time-of-day>, the preceding method will be invoked, because it is more specic than the method that Dylan generated on the <sixty-unit> class. If the new value for the total-seconds slot is valid (that is, is greater than or equal to 0), then this method calls next-method, which invokes the setter method on <sixty-unit>. If the new value is less than 0, an error is signaled. The following example show what happens when you call total-seconds-setter with a negative value for total-seconds:
? begin let test-time-of-day = make(<time-of-day>); test-time-of-day.total-seconds := -15;
82
This setter method ensures that no one can assign an invalid value to the slot. For completeness, we must also ensure that no one can initialize the slot to an invalid value. The way to do that is to dene an initialize method, as shown in Initialize methods. Considerations for naming slots and other objects A binding is an association between a name and an object. For example, there is a binding that associates the name of a constant and the value of the constant. The names of functions, module variables, local variables, and classes are also bindings. There is a potential problem that can occur if you use short names. If a client module uses other modules that also dene and export bindings with short names, there is a signicant chance that name clashes will occur, with different bindings with the same name being imported from different modules. If you use the Dylan naming conventions, then a variable will not have the same name as a class, a function, or a constant. The naming conventions avoid name clashes between different kinds of objects. A slot is identied by the name of its getter. The getter is visible to all client modules. There is no problem if two getters with the same name are dened by unrelated classes, because the appropriate getter is selected through method dispatch. There is a problem if a getter has the same name as a generic function with an incompatible parameter list or values declaration. (See Parameter-list congruence.) When such a problem occurs, the only way to resolve it is to use options to define module to exclude or rename some of the problem bindings. This solution is undesirable, because it requires work on the part of the author of the client module, who must spot and resolve such clashes, and then use an interface that no longer matches its documentation. Therefore, for getters that you intend to export, it makes sense prevent clashes by considering the name of the slot carefully. One technique is to prex the name of the property with the name of the class. For example, you might dene a <person> class with a slot person-name, instead of the shorter possibility, name. One drawback of this technique is that it might expose too much information about the implementation that is, the name betrays the class that happens to implement the slot at a particular time, and you have to remember which superclass introduces a property if you are to access that property. There is a compromise between using short names and using the class name as a prex you can choose a prex for a whole group of classes beneath a given class. For example, you might use the prex person- for slots of many classes that inherit from the <person> class, including <employee>, <consultant>, and so on.
define class <person> (<object>) slot person-name; slot person-age; end class <person>; define class <employee> (<person>) slot person-number; slot person-salary; end class <employee>; define class <consultant> (<employee>) slot person-perks; slot person-parking-lot; end class <consultant>;
Now, in a method on <consultant>, all accesses are consistent, and we do not have to remember where the slots actually originate:
// Method 1 define method person-status (p :: <consultant>) => (status :: <integer>)
4.2. Slots
83
If we had dened the classes differently, such that we prexed each getter with the name of the class that dened it, the method would look like this:
// Method 2 define method person-status (p :: <consultant>) => (status :: <integer>) (p.consultant-perks.evaluation + p.employee-salary.evaluation) / p.person-age; end method person-status;
Method 2 is more difcult to write and read than is Method 1, and is more fragile. If, at some point, all employees are allocated perks, then the use of the consultant-perks getter becomes a problem. Comparison with C++: In C++, the class is the namespace of its member functions. In Dylan, the module is the namespace of getters and setters. In general, the module is the namespace of all module bindings, including generic functions; getters and setters are generic functions.
On line 2, we call next-method. All methods for initialize should call next-method as their rst action, to allow any less specic initializations (that is, initialize methods dened on superclasses) to execute rst. If you call next-method as the rst action, then, in the rest of the method, you can operate on an instance that has been properly initialized by any initialize methods of superclasses. If you forget to include the call to next-method, your initialize method will be operating on an improperly initialized instance. Lines 3 through 6 contain the real action of this method. We check that the value is valid. If it is invalid, we signal an error. The following example shows what happens when total-seconds is not valid when we are creating an instance:
? make(<time-of-day>, total-seconds: -15); => ERROR: -15 is invalid. total-seconds cannot be negative.
84
When we use make to create any subclass of <sixty-unit> (such as <time-of-day>), and we do not supply the total-seconds: keyword to make, the total-seconds slot is initialized to 0. The init-value: slot option species an expression that is evaluated once, before the rst instance of the class is made, to yield a value. Every time that an instance is made and the slot needs a default value, this same value is used as the default. In general, a slot receives its default initial value when no init keyword is dened or when the caller does not supply the init-keyword argument to make. The required-init-keyword: slot option Instead of giving the slot a default initial value, we can require the caller of make to supply an init keyword for the slot. The required-init-keyword: slot option denes a required init keyword. If the caller of make does not supply the required init keyword, then an error is signaled.
define abstract class <sixty-unit> (<object>) slot total-seconds :: <integer>, required-init-keyword: total-seconds:; end class <sixty-unit>;
The total-seconds slot is dened in the <sixty-unit> class. By making total-seconds: a required init keyword in this class, we make it required for every class that inherits from it, including <time>, <angle>, and all their subclasses. Slot options for an inherited slot You can dene a slot in only one particular class in a set of classes related by inheritance. You can use the inherited slot specication to override the default initial value of an inherited slot, or the init function of an inherited slot. See The init-function: slot option. In this example, assume that the <sixty-unit> class denes the total-seconds slot and the init keyword total-seconds:, and provides the default initial value of 0 for that slot, as shown:
4.2. Slots
85
define abstract class <sixty-unit> (<object>) slot total-seconds :: <integer>, init-keyword: total-seconds:, init-value: 0; end class <sixty-unit>; define abstract class <time> (<sixty-unit>) end class <time>;
The <time-offset> class provides a different default initial value for the inherited slot total-seconds:
define class <time-offset> (<time>) inherited slot total-seconds, init-value: encode-total-seconds(1, 0, 0); end class <time-offset>;
By using the inherited slot specication, we are not dening the slot, but rather are stating that this slot is dened by a superclass. We can then provide either a default initial value or an init function for the inherited slot. The init-function: slot option We can use the init-function: slot option to provide a function of no arguments to be called to return a default initial value for the slot. These functions are called init functions. They allow the initial value of a slot to be an arbitrary computation.
define class <time-of-day> (<time>) inherited slot total-seconds, init-function: get-current-time; end class <time-of-day>;
Every time that we make an instance of the <time-of-day> class and we need a default value for the total-seconds slot, the get-current-time function is called to provide an initial value. Here, we assume that get-current-time is available as a library function; it is not part of the core Dylan language. The init-function: slot option species an expression that is evaluated once, before the rst instance of the class is made, to yield a function. The function must have no required arguments and must return at least one value. Every time that an instance is made and the slot needs a default value, this function is called with no arguments, and the value that it returns is used as the default. An init function is called during instance creation when no keyword argument is dened or when an optional keyword argument is not passed to make. Init expressions An init expression is another way of providing a default slot value. Here is an example:
define class <time-of-day> (<time>) inherited slot total-seconds = get-current-time(); end class <time-of-day>;
Every time that we make an instance of the <time-of-day> class and we need a default value for the total-seconds slot, the expression get-current-time(); is evaluated to provide an initial value. An init expression species an expression. Every time that an instance is made and the slot needs a default value, this expression is evaluated and its value is used as the default. Notice the similarity between the init-function: slot option and an init expression. In fact, the following slot specications are equivalent:
inherited slot total-seconds, init-function: get-current-time; inherited slot total-seconds = get-current-time();
86
That substitution works for functions that have no required arguments. More generally, the following slot specications are equivalent:
slot slot = expression; slot slot, init-function: method () expression end method;
The expression can be a call to a function that requires arguments. Here, we use method to dene a method with no name. The init-value: slot option, init-function: slot option, and init expression are mutually exclusive. A given slot specication can have only one of these.
The slot cruising-speed is dened with the each-subclass slot allocation. We use each-subclass allocation to express that, for example, all instances of Boeing 747 aircraft share a particular cruising speed, and all instances of McDonnell Douglas MD-80 aircraft share a particular cruising speed, but the cruising speed of 747s does not need to be the same as the cruising speeds of MD-80s.
4.2. Slots
87
rather than stored. You can optionally dene a setter method. If you want to initialize a virtual slot when you create an instance, you can dene an initialize method. We can use virtual slots to control the access to a slot. For example, we want to ensure that the value of the direction slot is north or south for <latitude>, and is east or west for <longitude>. (An alternative technique is to use enumeration types, as shown in Enumerations.) To enforce this restriction, we must Check the value when the setter method is invoked. In this section, we show how to do this check using a virtual slot. We also show how to use symbols, instead of strings, to represent north, south, east, and west. Check the value of the direction slot when an instance is created and initialized. We do that checking in Initialize method for a virtual slot. We redene the <directed-angle> class to include a virtual slot and an ordinary slot:
define abstract class <directed-angle> (<angle>) virtual slot direction :: <symbol>; slot internal-direction :: <symbol>; end class <directed-angle>;
We dene the slot direction with the virtual slot allocation. Notice that the slots allocation appears before the name of the slot (as contrasted with slot options, which appear after the name of the slot). In the <directed-angle> class, we use the slot internal-direction to store the direction. We shall provide a setter method for the virtual slot direction that checks the validity of the value of the direction before storing the value in the internal-direction slot. Symbols Symbols are much like strings. A symbol is an instance of the built-in class <symbol>. The key difference between strings and symbols lies in the way similarity (as tested by = ) and identity (as tested by == ) are dened for each of them. Two string operands can be similar but not identical. However, two symbol operands that are similar are always identical that is, they always refer to the same object. There are two reasons to use symbols in certain cases where you might consider using strings. First, symbol comparison is not case sensitive. Second, comparison of two symbols is much faster than is comparison of two strings, because symbols are compared by identity, and strings are usually compared element by element. In the <directed-angle> class, we dene the type of the two slots as <symbol>, instead of <string>, which we used in previous versions of this class. If we use strings, then when we checked whether the direction slot of a latitude was "north" or "south", we would have to worry about uppercase versus lowercase. For example, we would have to decide whether each of these were valid values: "north", "NORTH", "North", "NOrth", and so on. We simplify that decision by using the <symbol> type instead of <string>. There are two equivalent syntaxes for specifying symbols: Examples of use of the keyword syntax are: north: and south:. Examples of use of the hash syntax are:#"north" and #"south". Here, we show that symbol comparison is not case sensitive:
? #"NORTH" == #"North"; => #t
88
It is our convention in this book to reserve the keyword syntax for keyword parameters, and otherwise to use the hash syntax. For example, we would give the call:
make(<latitude>, direction: #"north")
Getter and setter methods for a virtual slot Here is the getter method for the virtual slot direction:
// Method 1 define method direction (angle :: <directed-angle>) => (dir :: <symbol>) angle.internal-direction; end method direction;
Here are the setter methods for the virtual slot direction:
// Method 2 define method direction-setter (dir :: <symbol>, angle :: <directed-angle>) => (new-dir :: <symbol>) angle.internal-direction := dir; end method direction-setter; // Method 3 define method direction-setter (dir :: <symbol>, latitude :: <latitude>) => (new-dir :: <symbol>) if (dir == #"north" | dir == #"south") next-method(); else error("%= is not north or south", dir); end if; end method direction-setter; // Method 4 define method direction-setter (dir :: <symbol>, longitude :: <longitude>) => (new-dir :: <symbol>) if (dir == #"east" | dir == #"west") next-method(); else error("%= is not east or west", dir); end if; end method direction-setter;
The preceding methods work as follows: When you call direction on an instance of <directed-angle> or any of its subclasses, method 1 is invoked. Method 1 calls the getter internal-direction, and returns the value of the internal-direction slot. When you call direction-setter on a direct instance of <latitude>, method 3 is invoked. Method 3 checks that the direction is valid for latitude; if it nds that the direction is valid, it calls next-method, which invokes method 2. Method 2 stores the direction in the internal-direction slot. When you call direction-setter on a direct instance of <longitude>, method 4 is called. Method 4 checks that the direction is valid for longitude; if it nds that the direction is valid, it calls next-method, which invokes method 2. Method 2 stores the direction in the internal-direction slot. 4.2. Slots 89
When you call direction-setter on a direct instance of <directed-angle>, method 2 is invoked. Method 2 stores the direction in the internal-direction slot. In these methods, we use dir, rather than direction, as the name of the parameter that represents direction. Recall that direction is the name of a getter. Although we technically could use direction as the parameter name in these methods (because we do not call the direction getter in the bodies), direction as a parameter name might be confusing to other people reading the code. The error function signals an error. For more information about signaling and handling errors, see Exceptions. The direction-setter methods check the direction when the setter is called. In Initialize method for a virtual slot, we check the direction when an instance is made. Initialize method for a virtual slot We dene the initialize method:
1 2 3 4
define method initialize (angle :: <directed-angle>, #key direction: dir) next-method(); angle.direction := dir; end method initialize;
For keyword parameters, the name of the keyword that you supply to make is normally the same name as the parameter that is initialized within the body. In this case, we want to avoid confusion between the getter direction and the keyword parameter direction:, so we use dir as the name of the keyword parameter for the initialize method. When you call make, you use the direction: keyword. However, within this method, the parameter is named dir. Line 3 calls the setter for the direction slot. We dened the methods for direction-setter in Getter and setter methods for a virtual slot. If the argument is a latitude, then method 3 is invoked to check the value. If the argument is a longitude, then method 4 is invoked to check the value. We can create a new instance of <absolute-position>.
? define variable *my-absolute-position* = make(<absolute-position>, latitude: make(<latitude>, total-seconds: encode-total-seconds(42, 19, 34), direction: #"north"), longitude: make(<longitude>, total-seconds: encode-total-seconds(70, 56, 26), direction: #"west"));
The preceding example works, because the values for direction are appropriate for latitude and longitude. The following example shows what happens when the direction is not valid when an instance is created:
? make(<latitude>, direction: #"nooth"); => ERROR: nooth is not north or south
The following example shows what happens when the direction is not valid when the direction setter is used:
? begin let my-longitude = make(<longitude>, direction: #"east"); my-longitude.direction := #"north"; end; => ERROR: north is not east or west
90
4.2.7 Summary
In this chapter, we covered the following: We described techniques for initializing slots; see Summary of slot-initialization techniques. We discussed the syntax of calling getters and setters; see Syntax of calling getters and setters. We showed how to dene methods for getters and setters. We showed how and why you can use symbols instead of strings. We described the different kinds of slot allocation; see Summary of slot allocations. Table 4.1: Summary of slot-initialization techniques Technique Summary initialize You can dene a method for initialize for a class to perform any actions to initialize the method instance. The make function calls the initialize generic function after make creates an instance and supplies those initial slot values that it can. If you need to do any complex computation to determine and set the value of a slot, you can do it in an initialize method. Init keyword You can use the init-keyword: slot option to declare an optional keyword argument, or the required-init-keyword: slot option to declare a required keyword argument for make when you create an instance of the class. The value of the keyword argument becomes the value of the slot. Init value You can use the init-value: slot option to give a default initial value for the slot. This option species an expression that is evaluated once, before the rst instance of the class is made, to yield a value. Every time an instance is made and the slot needs a default value, this same value is used as the default. The slot receives its default initial value when no init keyword is dened, or when the caller does not supply the init-keyword argument to make. Init function You can use the init-function: slot option to provide a function that returns a default value. This option species an expression that is evaluated once, before the rst instance of the class is made, to yield a function. The function must have no required arguments and must return at least one value. Every time that an instance is made and the slot needs a default value, this function is called with no arguments, and the value that it returns is used as the default. The slot receives its default initial value when no init keyword is dened or when the caller does not supply the init-keyword argument to make. Init You can use an init expression to provide an expression that yields a default value. Every time expression that an instance is made and the slot needs a default value, this expression is evaluated, and its value is used as the default. The slot receives its default initial value when no init keyword is dened, or when the caller does not supply the init-keyword argument to make. Table 4.2: Syntax of calling getters and setters Call object.function-name *my-time-of-day*.total-seconds; object.name := new-value; name(object) := new-value; *my-time-of-day*.total-seconds := 0; total-seconds(*my-time-of-day*) := 0; Translation function-name(object) total-seconds(*my-time-of-day*); name-setter(new-value, object); name-setter(new-value, object); total-seconds-setter (0, *my-time-of-day*); total-seconds-setter(0, *my-time-of-day*);
4.2. Slots
91
Table 4.3: Summary of slot allocations Allocation Instance Virtual Class Eachsubclass Summary Each instance allocates storage for the slot, and each instance of the class that denes the slot has its own value of the slot. Instance allocation is the default. No storage is allocated for the slot. You must provide a getter method that computes the value of the virtual slot. The class that denes the slot allocates storage for the slot. All general instances of the class share the value of the slot. The class that denes the slot and each of its subclasses allocate storage for the slot. All the direct instances of each class share the value of the slot.
92
The syntax *greeting*[n] refers to the n th element of the string in *greeting*. You can use this syntax to access any element of any collection. In Dylan, double quotes are used to enclose literal strings, and single quotes are used to enclose characters. We can use the assignment operator to change an element of a string:
? *greeting* := copy-sequence(*greeting*); =>"Hello, world." ? *greeting*[0] := j; => j ? *greeting*; => "jello, world."
We copied the greeting before modifying it, because modifying a literal constant is an error. A literal constant is an object whose contents are known completely at compile time. Dylan has a special syntax for each class of literal constant, so that they can be identied easily. The literal constant "Hello, world.", which is used to initialize the *greeting* variable, is part of the program executable, and is allocated when you compile the program. The copy-sequence generic function returns a new collection with the same elements as its argument. The
93
copy-sequence function creates the copy at run time, so modication of its results is permitted, because such changes do not alter the program itself. Note that, although the listener presents all objects in literal-constant syntax, not everything displayed by the listener is a literal constant. The square-bracket syntax is an abbreviation for calling the generic function element. The following examples are equivalent:
? *greeting*[0]; j ? element(*greeting*, 0); => j
You can use either the square-bracket syntax or the element generic function on any collection. You must be careful if you use element as a local variable, however, because doing so will interfere with its use as a generic function, including the use of the square-bracket abbreviation. Creation of vectors and access to elements There are several ways to create collections. One way is to create a collection by using make. For example, here we create a vector that contains two elements:
? define variable *my-vector* = make(<vector>, size: 2);
If you want to create a sequence of a certain size, with every element having the same value, you can specify a fill keyword argument to make. The default value for the fill keyword parameter is #f. Thus, if you had read an element of *my-vector* before you wrote numbers into it, you would have received #f. We can create and initialize a vector to different values all at once by using a built-in constructor. A constructor is a function that creates an instance; using it is a shorthand for calling make. Here, we use the vector constructor function to create a vector and to initialize it with data.
? define variable *my-vector* = vector(5, 3); ? *my-vector*; => #[5, 3]
As we saw in Creation of strings and access to elements, certain collections have a literal syntax that enables you to specify a particular data structure as part of the program:
? define variable *my-vector* = #[5, 3]; ? *my-vector*; => #[5, 3]
Diagram of the vector #[5, 3]. shows how you can picture the vector that we just created. You might think that *my-vector* is a direct instance of <vector>, but it is not: The <vector> class is abstract, but instantiable. When you use the vector function, or use make with <vector>, the result is a general instance 94 Chapter 4. Part 2. Intermediate Topics
Figure 4.2: Diagram of the vector #[5, 3]. of <simple-object-vector>. You specify the size of a <simple-object-vector> when you create one, and you cannot change that size later. If you need a vector that can change size, use the <stretchy-vector> class. See A new collection class, for an example that uses stretchy vectors. Creation of lists and access to elements Lists are similar in purpose to vectors: Each one can store an ordered sequence of objects. Lists differ from vectors in that it is easy to add and remove elements from lists, especially at the front. In general, if the number of elements in a sequence will remain constant, lists are less efcient than vectors are. Each element of a list is stored in a pair. A pair has two parts a head and a tail. Typically, the head of a pair refers to an element, and the tail refers to the pair that holds the next element of the list. Normally, the nal tail of the list is the empty list, represented by #(). Elements of lists can be any kind of object, including, of course, lists. The list constructor function creates a list whose elements are the arguments provided:
? list(4, 5, 6); => #(4, 5, 6)
Diagram of the list #(4, 5, 6). is a diagram of the list that we just created.
Figure 4.3: Diagram of the list #(4, 5, 6). We can create a similar list by using the pair function, which creates one pair of the list at a time:
? pair(4, pair(5, pair(6, #()))); => #(4, 5, 6)
As you can see, using list instead of pair, in this case, is much clearer. Note that Dylan provides functions called head and tail, which operate on lists:
? head(#(4, 5, 6)); => 4 ? tail(#(4, 5, 6)); => #(5, 6) ? tail(tail(#(4, 5, 6))); => #(6)
A reference to the rst pair of a list is exactly the same as a reference to the entire list.
95
We use head and tail when we dene a method for copying lists in Lists and efciency. We use pair in a method that copies lists recursively in A recursive list copier.
The method my-copy-sequence makes a new sequence of the same size as its argument, then iterates over all the elements of the argument, storing each element of the sequence into the appropriate element of the new sequence. The size generic function returns the number of elements in a collection. In this example, the while loop terminates when index reaches the size of the sequence. The type-for-copy generic function returns an appropriate class for make, given an object that you wish to copy. For most collections, type-for-copy just returns the class of the collection provided. Iteration with for We can use the for to express concisely a loop that increments a variable until a limit is reached.
define method my-copy-sequence (old-sequence :: <sequence>) => (new-sequence :: <sequence>) let new-sequence = make(type-for-copy(old-sequence), size: old-sequence.size); for (index from 0 below old-sequence.size) new-sequence[index] := old-sequence[index]; end for; new-sequence; end method my-copy-sequence;
In the preceding example, the body is executed old-sequence.size times, with index bound to zero rst, then rebound to one more than the previous value of index each time through the loop. The variable index is dened only within the body of the for iteration construct. The body of the for iteration construct begins after the iteration clause(s), and nishes with the matching end. For the while iteration construct shown in Building our own copy-sequence, the body starts after the predicate and nishes with the matching end. The for loop can have many different kinds of iteration clauses. In this section, we have shown a simple iteration over a series of numbers. In Lists and efciency, we use clauses that bind variables to initial values for the rst time 96 Chapter 4. Part 2. Intermediate Topics
through a loop, and use expressions to rebind the variables for the second and subsequent times through the loop. We also demonstrate a clause that permits iteration to continue until an expression is true, both in Lists and efciency and Adding and removing elements. The for loop has a simple type of iteration clause that we can use to iterate over any Dylan collection. The airport example in Vehicle containers, demonstrates iteration over vectors using this kind of iteration clause. Lists and efciency The my-copy-sequence method in Iteration with for works efciently for vectors. It does so because Dylan can store and retrieve arbitrary elements of vectors, and can determine the size of vectors in constant time. Lists are quite a different data structure from vectors. Accessing elements and determining the size of a list takes linear time. Thus, you can access the thousandth element of a vector or string in the same amount of time as you can access the rst element of a vector or string; when you uses lists, however, it takes about 1000 times longer to access the thousandth element than to access the rst element. The difference in access times occurs because Dylan must walk over almost 1000 pairs to get to the thousandth pair, and thus get to the thousandth element of the list. Although the method dened in Iteration with for can copy lists, it will be excessively slow, especially for long lists. We would like to provide a special method for copying lists that uses a more efcient algorithm. In particular, we want to walk over the provided list element by element, without having to retrace over elements of the list that we have already copied.
// Assumes that old-list is a proper list (that is, it ends with #()) // and is not circular define method my-copy-sequence (old-list :: <list>) => (new-list :: <list>) let new-list = make(<list>, size: old-list.size); for (old = old-list then old.tail, new = new-list then new.tail, until: empty?(old)) new.head := old.head; end for; new-list; end method my-copy-sequence;
First, my-copy-sequence makes a new list that is the same length as the old one. Next, the for iterator is used to bind the variables old and new to old-list and new-list, respectively. Then, the for iterator executes the until: expression to determine whether it is time to terminate the loop. If the until: expression returns true, then the for loop terminates, and the newly created list is returned from my-copy-sequence. Otherwise, the body of the for loop is executed the body stores the head of the rst pair in old into the head of the rst pair in new. The result of that action is that the rst element of new is identical to the rst element of old. For this iteration, that action causes the rst element of new-list to be identical to the rst element of old-list. In subsequent iterations, the body will access elements 1 closer to the end of the list. It will do so because, after the body is executed, the for iterator loops back to the iterator clauses, where the then clauses bind old to all but the rst pair of old, and bind new to all but the rst pair of new. The termination check occurs again, with the same consequences, depending on the value of the until: expression. Iteration then continues just like the second time through the loop until the end of old is reached. In this method, we never have to search for the current spot of the old list that we are copying, or to search for the end of the new list that we are building. The variables old and new track exactly which pairs in the iteration to access, and that tracking saves a considerable amount of time for large lists. When the iteration is nished, my-copy-sequence returns the new list.
97
Polymorphism An important advantage of programming in Dylan is that we can provide a general method for copying a sequence (as shown in Iteration with for), and also can provide special copying methods for particular subclasses of sequences (as shown in Lists and efciency). Method dispatch takes care of picking the best method for the argument. Callers of my-copy-sequence do not need to worry about any performance optimizations that we have installed for lists. They simply use my-copy-sequence for lists, just as they would for any other sequence. This polymorphism can be useful for keeping interfaces between components of a program simple and extensible. Mapping functions Iterating over all the elements of a collection is a common idiom, and Dylan provides several different mapping functions that accomplish these kinds of iterations in different ways. In the following example, we redene the my-copy-sequence method originally dened in Lists and efciency. Here, we use the do iteration construct, instead of a for loop.
// Assumes that old-list is a proper list (that is, it ends with #()) // and is not circular define method my-copy-sequence (old-list :: <list>) => (new-list :: <list>) let new-list = make(<list>, size: old-list.size); // Remember the pair of the copy that we are initializing let current-pair = new-list; // Iterate over all the elements of the existing list, making new pairs, // and splicing them into the end of the copy that we are building do(method (old-element) current-pair.head := old-element; current-pair := current-pair.tail; end method, old-list); new-list; end method my-copy-sequence;
The do mapping function takes a function and one or more collections, and calls the function on each element of each collection. The function should take one argument if you provide do with one collection, two arguments if you provide two collections, and so on. The result of calling the function is ignored, and do itself returns no meaningful value. The do function is useful only if the method that you provide accomplishes a valuable side effect. In the preceding example, the supplied method stores an element of the old list into the head of the current pair of the new list, and moves to the next pair of the new list. Note that this method is actually a closure, which closes over the current-pair local variable. See Closures, for more information about closures. A recursive list copier In many situations, the most concise way to manipulate lists (and other treelike structures) is to use recursion. In recursion, a function calls itself, directly or indirectly. In the following example, we redene the my-copy-sequence method for lists to use recursion instead of iteration.
define method my-copy-sequence (old-list :: <list>) => (new-list :: <list>) if (empty?(old-list)) #(); else pair(old-list.head, my-copy-sequence(old-list.tail)); end if; end method my-copy-sequence;
Note that recursion can be just as efcient as iteration. For example, consider the function my-reverse, which creates a new list with elements in the reverse order from the list you supply. 98 Chapter 4. Part 2. Intermediate Topics
define method my-reverse (old-list :: <list>) => (reversed-list :: <list>) local method rev (old :: <list>, results :: <list>) if (empty?(old)) results else rev(old.tail, pair(old.head, results)) end; end method; rev(old-list, #()); end method my-reverse;
The local method declaration inside the my-reverse method denes a function that is bound to the name rev only within a scope of the body of my-reverse. This declaration is different from define method, which creates module bindings that can be accessed outside the lexical scope of where they are dened. The local method rev calls itself as the last expression in its body. Thus, the rev method can be optimized by the Dylan compiler into code that is exactly as efcient as if it was written with iteration. Alternative ways of dening the my-reverse function are discussed in Reversal of sequences. Using map and curry Perhaps the easiest way to implement our simple sequence copier is to use the map function. The map function takes the same arguments as does do. However, instead of ignoring the return value of the function that you provide, map gathers into a new collection all the results of calling the provided function. The new collection will be an instance of the type-for-copy of the rst collection argument to map.
define method my-copy-sequence (old-sequence :: <sequence>) => (new-sequence :: <sequence>) map(identity, old-sequence); end method my-copy-sequence;
The identity function simply returns its argument without making any changes. A more interesting example is to dene a method that multiplies a number by each element of a vector, yielding a new vector with the products. Here is a sample call to scalar-multiply, which we dene next:
? scalar-multiply(3, #[4, 5, 6]); => #[12, 15, 18]
We use the method statement to create a kind of function (a closure) that multiplies scalar by an element of the vector provided by map. The map iterator then calls that function on each element of old-vector, collecting the results in a new sequence. A variant of map, called map-into, replaces elements in an existing collection, rather than creating a new collection for the results. See Basic collection methods, for an example of the use of map-into. We can dene this method more succinctly using curry, which is a function that generates a function:
define method scalar-multiply (scalar :: <number>, old-vector :: <vector>) => (result :: <vector>) map(curry(\*, scalar), old-vector); end method scalar-multiply;
The curry function in this example creates exactly the same method as the one that we created in the previous denition of scalar-multiply. That is, curry(\*, scalar) builds a function that multiplies its argument by scalar. This generated function is then used by map to compute the value of each element of the new sequence.
99
Mapping functions such as do and map work well when you want to operate over the entire collection. The map function works well only if there is a one-to-one correspondence between input-collection sizes and output-collection size. However, the other techniques that we have presented, such as using for and while, can work better when you want to operate on only part of a sequence. In A sequence copier that can copy a portion of a sequence, we take another look at how a for loop can help us to solve the problem of iterating over only part of a collection. A sequence copier that can copy a portion of a sequence The copy-sequence generic function provided by Dylan actually takes keyword arguments that allow only a portion of the sequence to be copied. Here is an example:
? copy-sequence("airport", start: 3); => "port" ? copy-sequence("snow", start: 1, end: 3); => "no"
In the following, we use a for loop with two iteration clauses to implement the more exible version of the general purpose my-copy-sequence:
define method my-copy-sequence (old-sequence :: <sequence>, #key start = 0, end: limit = old-sequence.size) => (new-sequence :: <sequence>) let new-sequence = make(type-for-copy(old-sequence), size: limit - start); for (source-index from start below limit, destination-index from 0) new-sequence[destination-index] := old-sequence[source-index]; end for; new-sequence; end method my-copy-sequence;
In the preceding example, we force the keyword parameter end: to bind the variable limit, rather than binding end. It is illegal to use end as a variable name, because end is one of a few reserved words in Dylan. In the body of the for loop, source-index will range from start to 1 less than limit, and destination-index will range from 0 to 1 less then limit minus start, which is the length of the new sequence being created. Changes to a generic functions signature Note that the my-copy-sequence method dened in A sequence copier that can copy a portion of a sequence has a parameter list that is not congruent with the parameter list of the generic function. That is, that method accepts the start: and end: keyword arguments, when previously only required arguments were allowed for that generic function. We did not explicitly dene the my-copy-sequence generic function; Dylan created the generic function implicitly, when we dened the rst method for it, in Building our own copy-sequence. The generic function accepts two required parameters, and no keyword parameters. When you need to change the signature of a generic function, you must change all the methods for that generic function to have a compatible signature. In our example, we would have to x the my-copy-sequence method for lists to accept the start: and end: keyword arguments, and would have to change the methods to operate on only a portion of the list provided. For more information about the congruence rules for methods of a generic function, see Parameter-list congruence.
100
After the call to reverse!, the value of *switch* is not dened. Only the return value from reverse! will be meaningful. If we want *switch* to contain the reversed sequence, we must instead write
? *switch* := reverse!(*switch*); => #["on", "switch"] ? *switch*; => #["on", "switch"]
Note that reverse! cannot change the object to which *switch* refers; however, reverse! is allowed to alter the contents of that object. Also note that reverse! may not return the same object as that you provide as its argument. Consider the case of using reverse! on a list to see how this behavior can be useful. Convention: Dylan has a convention of putting an exclamation point at the ends of the names of functions that can destructively modify their arguments. For example, reverse! takes a sequence, and returns a sequence that has the same elements but in reverse order. The reverse! generic function may change the sequence that is its argument. In contrast, the reverse generic function performs a similar operation, but does not destructively modify its argument. Setters are an exception to this convention: They modify their argument, but do not typically end with !. How can we write our own version of reverse using the iteration techniques presented so far?
define method my-reverse (seq :: <sequence>) => (reversed-seq :: <sequence>) let reversed-seq = make(type-for-copy(seq), size: seq.size); for (destination-index from seq.size - 1 to 0 by -1, source-index from 0) reversed-seq[destination-index] := seq[source-index]; end for; reversed-seq; end method my-reverse;
101
Once again, this algorithm is ne for vectors and strings, but has poor performance for lists. Here is a special my-reverse method for lists:
define method my-reverse (old-list :: <list>) => (reversed-list :: <list>) let reversed-list = #(); for (old-element in old-list) reversed-list := pair(old-element, reversed-list); end for; reversed-list; end method my-reverse;
It is easy to build up a list from its end to its start, and that is exactly what we do in the preceding method. We start with the empty list, and add pairs to the reversed list whose heads are the elements of the argument. We follow the old list from its start to its end, while we build the new list from its end to its start, thus reversing the list. It is important to remember that, even though we created a new sequence to contain the elements of the old sequence, we still share those old elements with the new sequence. If two elements of a collection refer to the same object, then modifying the element of one of the collections affects the value of the element of the other collection. We illustrate this behavior in Destructive operations and shared structure. Destructive operations and shared structure Consider the following example, and Figures State before the element is changed. and State after the element is changed..
// First we construct a vector of two vectors ? define variable *switch-states* = vector(vector("switch", "on"), vector("switch", "off")); ? *switch-states*; => #[#["switch", "on"], #["switch", "off"]] // Now, we reverse the vector, holding on to the result ? define variable *rev-switch-states* = my-reverse(*switch-states*);
At this point, the states of the variables and vectors correspond to State before the element is changed.. We examine the two sequences:
? *rev-switch-states*; => #[#["switch", "off"], #["switch", "on"]] // Although *switch-states* and *rev-switch-states* are different vectors, // they share elements ? *switch-states* == *rev-switch-states*; => #f
At this point, the states of the variables and vectors correspond to State after the element is changed.. We can look at the values of the variables: 102 Chapter 4. Part 2. Intermediate Topics
103
? *switch-states*; => #[#["master switch", "on"], #["switch", "off"]] ? *rev-switch-states*; => #[#["switch", "off"], #["master switch", "on"]]
Each object pictured in Figures State before the element is changed. and State after the element is changed. is a vector. The strings in the gures are vectors, although we did not draw them as such, to keep the diagrams relatively simple. Variables are not objects in Dylan, but they are shown referring to objects. In State after the element is changed., the string "switch" is not referenced by any other object and is therefore garbage; eventually, it will be reclaimed by a garbage collector. Changing an element of one collection can affect another collection if the two collections share elements. Two collections share an element if there is a value in one collection that is == (that is, identical) to a value in the other collection. Functions such as copy-sequence and reverse do only a shallow copy of their arguments: only the top level of the copy is new. Every other part is shared with the old sequence. Thus, it is important to take care when you modify objects that might be shared with other parts of your application. Using well-dened module boundaries that specify whether data structures can be modied by clients of the module can help you to keep application data consistent.
We can dene the interpret-votes method using the if control structure and the else clause:
define method interpret-votes (#key yes :: <nonnegative-integer> = 0, no :: <nonnegative-integer> = 0) => (interpretation :: <string>) if (yes > 0 & no = 0) "unanimously approved"; else if (yes > no) "approved"; else if (yes = no) "tie"; else "not approved"; end if; end if;
104
We dened the <nonnegative-integer> type in Examples of types that are not classes, using limited. Only positive integers and the integer 0 are instances of <nonnegative-integer>. We use this type in the interpret-votes method parameter list to ensure that no negative vote counts are accepted. Quick summary of & inx operator : arg1 & arg2 The inx operator & does the and logical operation. If either or both of the arguments to the & operator are false, then & returns false. Note that the & operator is actually a control-ow operator. If the rst argument to the & operator is false, then the value of the second argument is never computed, and false is returned. If the value of the rst argument is true, then the value of the second argument is computed and returned. The | operator (logical or) behaves in a similar manner, except that its second argument is computed and returned only if the rst argument is false. The syntax for the if control structure allows elseif clauses, which makes this style of conditionalization slightly more compact:
define method interpret-votes (#key yes :: <nonnegative-integer> = 0, no :: <nonnegative-integer> = 0) => (interpretation :: <string>) if (yes > 0 & no = 0) "unanimously approved"; elseif (yes > no) "approved"; elseif (yes = no) "tie"; else "not approved"; end if; end method interpret-votes;
Branching with case Dylan also provides the case control structure to give you an alternative way to express the branching style shown in if, else, and elseif:
define method interpret-votes (#key yes :: <nonnegative-integer> = 0, no :: <nonnegative-integer> = 0) => (interpretation :: <string>) case (yes > 0 & no = 0) => "unanimously approved"; (yes > no) => "approved"; (yes = no) => "tie"; otherwise => "not approved"; end case; end method interpret-votes;
The decision of whether to use if with elseif and else as opposed to using case, is largely a matter of personal style.
105
Branching with select In certain situations, you are working with a particular two-argument predicate (such as == or <). The value of the rst argument to the predicate will always be the same, and you would like to perform different actions based on the second value. You can use both if and case to handle this situation, but the select control structure is more concise. The following example interprets trafc-light colors:
define method color-action (color :: <symbol>) => (action :: <symbol>) select (color) #"red" => #"stop"; #"yellow" => #"slow"; #"green" => #"go"; end select; end method color-action;
The select control structure uses == for the default predicate. For example, in the preceding select statement, the symbol #"stop" will be returned if color == #"red". If you require a different predicate, use the by clause, as shown in the following example, which interprets age from a number representing years:
define method interpret-age (age :: <nonnegative-integer>) => (description :: <string>) select (age by \<) 13 => "youngster"; 20 => "teenager"; 60 => "adult"; otherwise => "senior"; end select; end method interpret-age;
The preceding method returns the string "youngster" when provided an age less then 13; returns "teenager" when the age is between 13 and 20; and returns "adult" when the age is between 20 and 60. In all other cases, it returns "senior". Tables: Dynamic associations In Branching with select, we saw how the color-action method associated trafc-light colors with actions by using select. These associations are static. They are determined at compile time, and you cannot change them without recompiling the color-action method. Sometimes, it is useful to associate one object with another dynamically, while the program is running. Collections are good data structures for this purpose. How could we rewrite color-action so that it uses a collection to associate colors with actions?
define variable *color-action-table* = make(<table>, size: 3); *color-action-table*[#"red"] := #"stop"; *color-action-table*[#"yellow"] := #"slow"; *color-action-table*[#"green"] := #"go"; define method color-action (color :: <symbol>) => (action :: <symbol>) *color-action-table*[color]; end method color-action;
The tables provided by Dylan use == to compare keys. During the execution of the program, we could add new associations to *color-action-table*, or could change or remove existing associations. Tables grow as necessary to accommodate new associations that are added.
106
Search of arrays with for and block Suppose that you wanted to search a two-dimensional array, and to return the rst number greater than a given value.
define method find-larger-than (2d-array :: <array>, value :: <integer>) => (result :: type-union(singleton(#f), <integer>)) let first-dimension = dimension(2d-array, 0); let second-dimension = dimension(2d-array, 1); block (return) for (i from 0 below first-dimension) for (j from 0 below second-dimension) if (2d-array[i, j] > value) return(2d-array[i, j]); end if; end for; end for; #f; end block; end method find-larger-than;
In the preceding example, the block statement binds the variable return to a nonlocal exit procedure. If this exit procedure is called while the block is in effect, it will return immediately from the block statement, using any provided arguments as return values. Thus, if an element of 2d-array is greater than value, then this element will be returned immediately from the block, and thus from the method. Array elements can be accessed with the square-bracket syntax, or with the function aref. (For more information about referencing elements of an array, see Element references.) If the entire array is searched, and no element is found that is greater than value, then the for loops exit normally and the block statement returns the last value in the block body, which in this case is false. We use the type-union type-generating function to create a type that permits either false or an integer to be returned from this method. Search of arrays with find-key In Dylan, we can access multidimensional arrays as though they are linearized one-dimensional vectors by using the element generic function. Dylan provides a find-key generic function that uses element to nd the index (or key) that corresponds to a desired value in a collection. Here, we rewrite find-larger-than to use find-key :
define method find-larger-than (array :: <array>, value :: <integer>) => (result :: type-union(singleton(#f), <integer>)) let index = find-key(array, method (array-element) array-element > value end); index & array[index]; end method find-larger-than;
The find-key generic function searches an array, calling the function that we provided on each element. If our function ever returns true, find-key returns the linearized index of the array element containing the value. For a two-dimensional array, the linearized index is the index that would be the appropriate key of a one-dimensional array that we could construct by placing the rows of the two-dimensional array one after the other. Rows in a twodimensional array are numbered with the rst subscript, and the column within those rows is numbered by the second subscript. If our function never returns true for any element, find-key returns false. In this example & is truly used as a control structure. If index is false, then & will return false without executing the array access. If index is true, then the array access occurs, and that is the value of the & expression, and thus the value returned from the method.
107
4.3.6 Summary
In this chapter, we covered the following: We showed a selection of built-in collection classes, including strings, lists, vectors, tables, and arrays. We showed various iteration facilities and control structures, including for, do, map, while, if, case, select, block, &, and |. We showed a simple example of recursion. We showed some basic collection functions: element, size, and find-key. We showed some basic sequence functions: copy-sequence, and reverse. We showed additional collection functions: head, tail, pair, list, and vector. We explored basic sequence algorithms, and found that, although the various sequence classes are related, algorithms that are efcient for one class of sequence may not be appropriate for a different class of sequence. We discussed destructive versus nondestructive functions. We demonstrated the curry function, which generates functions. We showed several examples of the use of closures as arguments to iterators.
4.4 Functions
Functions are ubiquitous in Dylan. Generic functions and methods the two kinds of function are the primary means of specialization. Many common operations, such as slot references and arithmetic operations, are accomplished through function calls. In Dylan, unlike in many languages, functions are rst-class objects. They can be the values of variables or slots, arguments to other functions, or values returned by functions. Dylan has functions that build new functions out of existing functions. Much of the power of Dylan arises through its sophisticated treatment of functions. This chapter discusses general aspects of the operation of functions in Dylan. It does not describe all aspects of functions. In particular, we discuss the process of method dispatch within generic functions elsewhere (see Sections Method dispatch, Method dispatch for multimethods, Method dispatch and nonclass types, and Multiple inheritance and method dispatch). This chapter covers three main topics: 1. The syntax of function calls, including abbreviations for function calls 2. The function-calling protocol, and particularly the interaction between a function and its caller 3. The uses of functions as objects, including ways of creating and operating on functions
108
The remainder of this section describes these syntactic forms and the equivalent function calls. Unless otherwise noted, all expressions that make up any of these function calls are evaluated from left to right. (A notable exception is an expression containing the assignment operator, discussed in Assignment.) The common left-to-right rule makes it easy to understand the order of execution of Dylan code. But it also means that certain syntactic forms that we call equivalent that is, syntactic forms that generally result in calls to the same function with the same arguments differ in the order of evaluation of their components. The components can appear in different orders in otherwise equivalent syntactic forms. Usually, the order of evaluation makes no difference, and you can use whichever of the equivalent syntactic forms you nd most convenient. Explicit function calls The Dylan syntax for an explicit function call has two parts: 1. The function to be called This is an operand that is evaluated to yield the function itself. Usually, the operand is a reference to a variable or constant that names the function, although it can be any expression (except an operator call) whose value is a function. (For information on operator calls, see Sections Unary operator calls and Binary-operator calls.) 2. The arguments to which the function is applied The arguments are represented by a series of expressions, enclosed in parentheses and separated by commas. Each expression is evaluated, and its value is passed to the function as an argument. In the following function call, the function is the value of the variable truncate/; the two arguments are the value of the variable n and the number 3:
truncate/(n, 3);
A function can be obtained in other ways: for example, it might be an element of an array, the value of a slot of an instance, or the value returned by a call to another function. The following example calls the function that is the element of an operations array designated by the constant $trunc:
operations[$trunc](n, 3);
Slot references A slot reference is a reference to the value of a slot of an instance. The syntax for a slot reference has two parts, separated by a period: An operand whose value is the instance The name of the slots getter generic function In the following slot reference, the function get-employee-named returns an instance, which has a slot whose getter is named employee-number:
get-employee-named("Jane").employee-number;
Note that the operand that yields the instance can itself be a slot reference, so slot references can be chained:
plant.manager.employee-number;
Every slot value in Dylan is obtained by a call to the slots getter generic function (although the compiler can often optimize this generic function call to a direct slot access). A slot reference is just an abbreviation for a function call. With one exception, the following examples are equivalent:
plant.manager; manager(plant);
4.4. Functions
109
The one difference between these examples is that, in the rst, plant is evaluated rst, whereas in the second, manager is evaluated rst. In fact, you can use the slot-reference syntax for more than slot references. The object that is the value of the left side can be any object, and the function named by the right side can be any function that can take the object as an argument. The function named by the right side is always called with the object that is the value of the left side as its only argument. Thus, using the plant.manager syntax is just another way of calling the function named by manager with the object that is the value of plant as the only argument. The plant object does not have to have a manager slot. In this book, we use slot-reference syntax for A call to a getter generic function for a slot A call to a function that takes one argument and returns one value that represents a property of an object Element references Collections in Dylan include such data structures as arrays, strings, lists, and tables. Each collection has a mapping from keys to elements. Dylans syntax for referring to an element of a collection has two parts: 1. An operand whose value is the collection 2. An expression, in square brackets, whose value is the key that maps to the desired element of the collection If the collection is a multidimensional array, the key expression in square brackets can be a series of expressions, separated by commas. Each expression yields the index for one dimension of the array. (Dylan array indices are zero based.) The following example returns the rst element of the array named by my-array:
my-array[0];
An element reference, like a slot reference, is an abbreviation for a function call. The generic function element takes a collection and a key as arguments, and returns the element of the collection that is associated with the given key. Except for the order of evaluation, the following examples are equivalent:
my-array[0]; element(my-array, 0);
For arrays of more than one dimension, the key expression in brackets is instead a comma-separated series of expressions. In this case, the element reference is an abbreviation for a call to the aref generic function. This function takes an array and any number of indices as arguments, and returns the element associated with the array indices. Except for the order of evaluation, the following examples are equivalent:
my-array[0, 2]; aref(my-array, 0, 2);
Unary operator calls Dylan has two built-in unary operators, - and ~. The syntax for a unary operator call has two parts: 1. The operator 2. An operand The - operator performs the arithmetic negation of its operand, and the ~ operator performs the logical negation. Both operator calls are abbreviations for function calls. The following examples are equivalent:
110
- time-offset; negative(time-offset);
In the preceding example, we must escape ~ with \ so that Dylan interprets ~ as a variable name, instead of as an operator. This syntax indicates an explicit call to the function that is the value of the variable named ~. Binary-operator calls Dylan has 16 built-in binary operators, of the following kinds: Arithmetic operations: +, -, *, /, and ^ Comparisons: =, ==, <, >, <=, >=, ~=, and ~== Logical operations: & and | Assignment: := The syntax for a binary-operator call has three parts: 1. An expression that serves as the rst operand 2. The operator 3. An expression that serves as the second operand All binary-operator calls, except those to the logical and assignment operators, are abbreviations for calls to functions that have the same names as do the operators. Except for the order of evaluation, the following examples are equivalent:
a + b; \+(a, b);
The & and | operators are implemented as macros. (For information on macros, see Macros.) In an expression that includes the & operator, if the rst operand has a false value, the second operand is not evaluated. In an expression that includes the | operator, if the rst operand has a true value, the second operand is not evaluated. Assignment The assignment binary operator, :=, also is implemented as a macro. An expression that includes this operator works in a special way. The operand to the right of the operator is evaluated rst. The result is the new value to be assigned. The operand to the left of the operator determines the place to which the new value is assigned. This operand can have one of the following kinds of syntax: Variable name The variable name is not evaluated. Dylan assigns the new value to the variable. Explicit function call Dylan calls the function name -setter, where name is the name of the function in the function call. The rst argument to name -setter is the new value, and the remaining arguments are the arguments to name in the original function call. Slot reference Dylan rst converts the slot reference to the corresponding function call. Dylan then calls the function name -setter just as it would have if the slot reference had been an explicit function call.
4.4. Functions
111
Element reference Dylan rst converts the element reference to the corresponding function call, using element or aref as the name of the function, as appropriate. Dylan then calls the function element-setter or aref-setter just as it would have if the element reference had been an explicit function call. Except for the order of evaluation and returned values, the following examples are equivalent:
*my-position*.distance := 3.0; distance(*my-position*) := 3.0; distance-setter(3.0, *my-position*);
The rst two examples return 3.0; the second returns whatever distance-setter returns. Usually, this value would be 3.0. Note that, if distance is the name of a slots getter, and if the slot is constant or has a setter with a name other than distance-setter, then the assignment operation results in an error. Except for the order of evaluation and returned values, the following examples are equivalent:
vertices[2] := list(3.5, 4.5); element(vertices, 2) := list(3.5, 4.5); element-setter(list(3.5, 4.5), vertices, 2);
112
2. The functions implicit body is exited, ending the scope of all local variables (including parameters) established in that body. 3. The values specied by the value declaration are returned to the caller of the function. (Depending on the value declaration, the number of values returned to the functions caller might be more or less than the number of values returned by the last expression in the functions body.) Note these two important implications of the way that arguments are passed: All bindings of arguments to parameters are local to the body of the function called. Assignment to a parameter inside the called functions body does not affect any variables outside the body that have the same name. For example, consider these denitions:
define method calling-function () let x = 1; let y = 2; format-out("In calling function, before call: x = %d, y = %d\n", x, y); called-function(x, y); format-out("In calling function, after call: x = %d, y = %d\n", x, y); end method calling-function; define method called-function (x, y) x := 3; y := 4; format-out("In called function, before return: x = %d, y = %d\n", x, y); end method called-function;
Although parameters are local to a function, all arguments and return values are shared between a function and its caller. If an argument or return value is a mutable object one that can be changed then any changes that a function makes to that object are visible to its caller. Consider the following denitions:
define class <test> (<object>) slot test-slot, required-init-keyword: test-slot:; end class <test>; define method calling-function () let x = make(<test>, test-slot: "before"); format-out("In calling function, before call: x.test-slot = %s\n", x.test-slot); called-function(x); format-out("In calling function, after call: x.test-slot = %s\n", x.test-slot); end method calling-function; define method called-function (x :: <test>) x.test-slot := "after"; format-out("In called function, before return: x.test-slot = %s\n", x.test-slot); end method called-function;
4.4. Functions
113
Note here that we have redened the calling-function method, and have dened a new called-function method, which we rst dened in the previous example. Our new called-function method has one parameter, whereas the previous method had two. The parameter list of this new method is not compatible with that of the previous method, and, if we actually tried to dene the second called-function method, Dylan would signal an error. For more information on compatibility of parameter lists for generic functions and methods, see Parameter-list congruence. A call to calling-function now produces the following output:
In calling function, before call: x.test-slot = "before" In called function, before return: x.test-slot = "after" In calling function, after call: x.test-slot = "after"
In this case, x in the calling function and x in the called function are different variables. But the values of both variables are the same object: the instance of <test> that we make in the calling function. The change to the slot value of this object that we make in the called function is visible to the calling function. It is equally proper to think of arguments that are immutable, like integers, as being shared between a function and its caller. By denition, however, a function cannot make any changes to such objects that are visible to the functions caller. Comparison with C and C++: As in Dylan, the parameters of a C function are local to the body of the function, and assignment to a parameter does not affect the value of a variable that has the same name in the functions caller. But the relationship between objects and values is not the same in C and in Dylan. In C, a value can be an object (roughly meaning the contents of the object) or a pointer to an object (roughly meaning the location of the object in memory). The value of a parameter in C is always a copy of the corresponding argument. When a C structure is an argument to a function, the value of the corresponding parameter is a copy of the structure; it is not the structure itself. If the function changes the value of a member of this structure, the change is not visible to the caller, because the function is changing only its own copy of the structure. But if the argument is a pointer to a structure, the function can gain access to the callers structure (by dereferencing the pointer). If the function changes the value of a member of such a structure by dereferencing the pointer, the change is visible to the caller. In Dylan, a value is always an object, which has a unique identity. The value of a parameter is always the same object as the corresponding argument. When a function changes such an object (as by changing the value of a slot), the change is always visible to the caller. Dylan has no equivalent to C pointers. In C++, a parameter declared using ordinary C syntax also receives a copy of a structure or an instance that is the corresponding argument. C++ has additional syntax for declaring that a parameter is a reference essentially an implicit pointer to the corresponding argument. In this case the argument is not copied, and if the function changes the object that the parameter refers to, the changes are visible to the caller. In some ways Dylans argument-passing protocol is similar to C++ references. In both C and C++, array arguments are always passed as pointers. In Dylan, arrays are instances of the <array> class, and array arguments are treated like all other arguments. For more comparisons between Dylan and C objects, see Dylan Object Model for C and C++ Programmers.
Return and reception of multiple values A Dylan function call and, in general, a Dylan expression can return any number of values, including none. The values function is the means of returning multiple values. This function takes zero or more arguments, and returns them as separate values. Multiple values can be received as the initial values of local variables in a let declaration. If a let declaration contains multiple variables, they are matched with the values returned by the initialization expression, and each variable is bound to the corresponding value. The following example initializes a to 1 and b to 2:
114
The following example initializes ans to 2 and rem to 1 the two values returned by this call to truncate/:
let (ans, rem) = truncate/(5, 2);
The variable list can also end with #rest followed by the name of a variable. In this case, the variable is initialized to a sequence. This sequence contains all the remaining values returned by the initialization expression. If there is no #rest, any excess values are discarded. If the number of variables in the let declaration is greater than the number of values returned, the remaining variables are initialized to #f. (But if the let declaration species a type for any of these variables, and if #f is not an instance of that type, then Dylan signals an error.) Module variables and constants can also be initialized to multiple values. The variable list of a define variable or define constant denition can contain multiple variables, and can receive multiple values from its initialization expression in the same way as a let declaration. Parameter lists A functions parameter list is specied in the function denition. (If Dylan implicitly denes a function, such as the getter and setter functions for a slot, Dylan also denes the parameter list for that function.) In a function denition, the parameter list follows the function name and consists of zero or more parameter specications, separated by commas and enclosed in parentheses. A parameter list can have three kinds of parameters: 1. Required parameters specify required arguments, or arguments that must be supplied when the function is called. All required parameters appear before other kinds of parameters in the parameter list. 2. A function can have at most one rest parameter, which allows the function to accept a variable number of arguments. The rest parameter is identied in the parameter list by #rest followed by the name of the parameter. When the function is called, all arguments that follow the required arguments are put into a sequence. This sequence is the initial value of the rest parameter in the function body. 3. Keyword parameters specify optional keyword arguments. In the parameter list, keyword parameters are identied by #key followed by the names of the parameters (and possibly by other information). Keyword parameters must follow all required parameters and the rest parameter (if any). When the function is called, the caller can supply any or none of the specied keyword arguments, in any order, after supplying all required arguments. The caller supplies each keyword argument as a symbol (usually in the form of the parameter name followed by a colon), followed by the argument value. This argument is the initial value of the corresponding keyword parameter in the function body. The specication for each parameter in the parameter list includes the name of the parameter. In addition, a required parameter (or, for a method, a keyword parameter) can be specialized to correspond to an argument of a given type. The type specializer follows the parameter name and is identied by :: followed by a type. When the function is called, the argument that corresponds to the parameter must be of the specied type, or Dylan signals an error. The default argument type is <object>. The specication for a keyword parameter can have two additional pieces of information: 1. It may include a keyword for the caller to use in its argument list, if this keyword must be different from the parameter name. The keyword precedes the parameter name in the parameter list. 2. It may include a default value for the keyword argument, which is used if the caller does not supply that argument. The default expression appears at the end of the parameter specication, followed by =. If no default expression is supplied and the caller does not supply the keyword argument, the arguments value is #f. The following example shows how we could use a rest parameter to implement a function to sum an arbitrary number of values:
4.4. Functions
115
// Sum one or more values define method sum (value, #rest more-values) for (next in more-values) value := value + next; end for; value; end method sum; ? sum(3); => 3 ? sum(1, 2, 3, 4, 5); => 15
In the preceding example, the for iteration statement performs the addition once for every element of more-values. The following example shows how we could use keyword parameters in dening a method similar to encode-total-seconds:
// Convert days, hours, minutes, and seconds to seconds. // Named (keyword) arguments are optional define method convert-to-seconds (#key hours :: <integer> = 0, minutes :: <integer> = 0, seconds :: <integer> = 0) => (seconds :: <integer>) ((hours * 60) + minutes) * 60 + seconds; end method convert-to-seconds; ? convert-to-seconds(minutes: 3, seconds: 9); => 189 ? convert-to-seconds(minutes: 1, hours: 2); => 7260
Note from the preceding example that we can supply keyword arguments in any order. Note also that all keyword arguments are optional; however, if we try to call a function with a keyword argument that the function does not accept such as days:, in this example Dylan signals an error. For more information on function calls and keyword arguments, see Keyword-argument checking. Following are additional features and restrictions of keyword arguments: If a parameter list ends with #all-keys following #key, the function accepts (but ignores) any keyword argument. A parameter list can have specic keyword parameters and also end with #all-keys. In this case, the function accepts any keyword argument, and also has local variables whose values are the keyword-argument values (or their defaults) that correspond to the keyword parameters. If the parameter list of a method contains both #rest and #key, the sequence that is the value of the rest parameter contains alternating symbols and argument values representing the keyword arguments passed to the function. In this case, all optional arguments must be keyword arguments. A generic functions parameter list can have either #rest or #key, but cannot have both. Keyword parameters for a generic function cannot be specialized. The restrictions on a generic functions parameter list have to do with parameter-list congruency and keywordargument checking in generic function calls. For more information, see Sections Parameter-list congruence and Keyword-argument checking.
116
Value declarations A function denitions value declaration follows the parameter list and is preceded by =>. The syntax of a value declaration is similar to that of a parameter list. If the function returns no values, the value declaration is an empty set of parentheses. Otherwise, the declaration can contain separate declarations for all returned values, separated by commas. Each of these individual declarations consists of a name and, optionally, :: followed by a type. The name does not specify a variable and has no use other than documentation. But the returned value that corresponds to the declaration must be of the declared type, or Dylan signals an error. The default return value type is <object>. A value declaration can also end with #rest followed by a name and, optionally, :: and a type. This declaration indicates that the function can return any number of additional arguments, each of which must be of the specied type. If a function has no explicit value declaration, the default declaration is (#rest x :: ration indicates that the function can return any number of arguments of any type. <object>). This decla-
The value declaration determines the number and types of values that the function returns, even if the last expression in the functions body returns a different number of values. If the functions body returns fewer values than are declared, the function defaults the remaining values to #f and returns them. (But if the value declaration species a type for any of these values, and if #f is not an instance of that type, Dylan signals an error.) If the functions body returns more values than are declared, the function returns the additional values if the declaration contains #rest; otherwise, the function discards the additional values. Parameter-list congruence A generic function and its methods must all have parameter lists that are compatible, or congruent. Following are the basic rules: A generic function and its methods must all have the same number of required arguments. The type of any given parameter in each method must be a subtype of the corresponding parameter in the generic function. If a generic function or any of its methods has only required arguments that is, it has neither #rest nor #key in its parameter list then the generic function and all its methods must have only required arguments. If a generic function or any of its methods accepts a variable number of arguments, but does not accept keyword arguments that is, it has #rest, but does not have #key, in its parameter list then the generic function and all its methods must accept a variable number of arguments, but must not accept keyword arguments. If a generic function or any of its methods accepts keyword arguments that is, it has #key in its parameter list then the generic function and all its methods must accept keyword arguments. For this rule, a generic function or method accepts keyword arguments even if its parameter list ends with just #key. If a generic function has any specic keyword parameters, then all its methods must have (at least) those specic keyword parameters. The appearance of #all-keys in a methods parameter list does not satisfy this requirement. The following parameter lists are congruent, because both functions have only required arguments, they have the same number of required arguments, and the type of each method parameter is a subtype of the same parameter in the generic function:
define generic g (arg1 :: <complex>, arg2 :: <integer>); define method g (arg1 :: <real>, arg2 :: <integer>) ... end method g;
The following parameter lists are congruent, because both functions meet the tests for required arguments, both accept keyword arguments, and the generic function has no specic keyword parameters:
4.4. Functions
117
define generic g (arg1 :: <real>, #key); define method g (arg1 :: <integer>, #key base :: <integer> = 10) ... end method g;
The following parameter lists are not congruent, because the methods parameter list does not include the specic keyword base of the generic function, even though it does include #all-keys:
define generic g (arg1 :: <integer>, #key base); define method g (arg1 :: <integer>, #key #all-keys) ... end method g;
Return-value congruence Like parameter lists, the value declarations of a generic function and that functions methods must be congruent. The rules depend on whether the generic function returns a xed or a variable number of values: If the generic function returns a xed number of values that is, it does not have #rest in its value declaration then its methods cannot have #rest, and must return the same number of required values as the generic function. For each method, the type of each returned value must be a subtype of the same returned value in the generic function. If the generic function returns a variable number of values that is, it has #rest in its value declaration then its methods can (but are not required to) have #rest, and must return at least as many required values as the generic function. For each method, the type of each returned value must be a subtype of the same returned value in the generic function. If the method has more required returned values than the generic function, their types must all be subtypes of the generic functions #rest value. The following value declarations are congruent, because the generic function implicitly returns any number of values of any type:
define generic g (arg1 :: <complex>, arg2 :: <integer>); define method g (arg1 :: <real>, arg2 :: <integer>) => (result :: <real>) ... end method g;
The following value declarations are not congruent, because the type of the methods returned value is not a subtype of the generic functions returned value:
define generic g (arg1 :: <complex>, arg2 :: <integer>) => (result :: <integer>); define method g (arg1 :: <real>, arg2 :: <integer>) => (result :: <real>) ... end method g;
Keyword-argument checking When a function is called, Dylan determines which keyword arguments, if any, are permitted for that function call. The set of permitted keyword arguments depends on whether or not a generic function is being called:
118
If a method is called directly, rather than through a generic function, the specic keywords in the methods parameter list are permitted. If the parameter list includes #all-keys, any keyword argument is permitted. If a generic function is called, all the specic keywords in the parameter lists of all applicable methods are permitted. If the parameter list of the generic function or of any applicable method includes #all-keys, any keyword argument is permitted. When a generic function is called, one of its methods is applicable if every required argument is an instance of the type of the corresponding parameter of the method. For more information on applicable methods, see Method dispatch. Consider the following denitions:
define generic g (arg1 :: <real>, #key); // Method 1 define method g (arg1 :: <real>, #key real-key) ... end method g; // Method 2 define method g (arg1 :: <float>, #key float-key) ... end method g; // Method 3 define method g (arg1 :: <integer>, #key integer-key) ... end method g;
Now, if we call the generic function g with an instance of <float>, we can supply the keyword arguments real-key: and float-key:, because the methods that have those keyword parameters are both applicable. If we call g with an instance of <integer>, we can supply the keyword arguments real-key: and integer-key:. Suppose that, in this same example, we call the generic function g with an instance of <float>, and supply the keyword arguments real-key: and float-key:. Method 2 is most specic, and is called as a result of Dylans method dispatch. But method 2 does not have a real-key: parameter. If we were calling this method directly, Dylan would signal an error. In this case, method 2 simply ignores the real-key: argument, because Dylan checks keyword arguments for a generic function call as a whole, rather than for a particular method chosen as a result of method dispatch. There is an important subtlety of keyword-parameter specications to note in this example. Because of the rules for parameter-list congruence, the generic function and all its methods must accept keyword arguments that is, they must all have #key in their parameter lists. Notice that we terminated the generic functions parameter list with #key. This use indicates that the generic function permits but does not require individual methods to specify keyword parameters. Suppose that we had instead terminated the generic functions parameter list with #key, #all-keys. This use also would have permitted, but would not have required, individual methods to specify keyword parameters. But it also would have allowed a caller of the generic function to supply any keyword argument. In the earlier example, only a small set of keyword arguments was permitted, and the members of the set varied with the applicable methods. In general, when you dene a generic function or a method that accepts keyword arguments, it is advisable not to specify #all-keys unnecessarily, because doing so defeats Dylans keyword-argument checking. If a method needs to accept keyword arguments because of the rules of parameter-list congruence, but does not need to recognize any keywords itself, you should terminate its parameter list with #key.
4.4. Functions
119
120
You can create one implicitly by dening a slot (other than a virtual or a constant slot) in define class. Dylan denes a setter method for the slot, and adds it to a generic function, creating the generic function if that function does not already exist. Creating a method by using method is useful when the method does not need to be part of a generic function. For instance, various Dylan functions take as arguments other functions that act as predicates, or test functions. One of these is choose, which selects members of a sequence that satisfy a test function, and returns those members as a new sequence. We might pick all the strings out of a mixed sequence as follows:
define method choose-strings (sequence :: <sequence>) => (new-seq :: <sequence>) // choose takes two arguments: a function and a sequence choose(method (object) instance?(object, <string>) end method, sequence); end method choose-strings;
Creating a method by using local method is useful for a method that does not need to be part of a generic function, but does need to be given a name so that it can call itself recursively, or so that other code in the enclosing body can refer to it. For an example, see A recursive list copier. Application of a function to arguments The Dylan function apply takes as arguments a function and one or more additional arguments, the nal one of which must be a sequence. The apply function calls its rst argument the function and passes that function the remaining arguments to apply. But instead of passing its nal argument as a sequence, it passes each element of the sequence as an individual argument. The apply function is perhaps most useful in the body of a function that receives a variable number of arguments and must pass those arguments to another function that also takes a variable number of arguments. For example, we can use apply to write a recursive version of the sum function that we dened iteratively in Parameter lists:
// Sum one or more values define method sum (value, #rest more-values) // If only one value, that is the answer if (empty?(more-values)) value; // Otherwise, add the first value to the sum of the rest else value + apply(sum, more-values); end if; end method sum;
Operations on functions Dylan has several functions that take functions as arguments, and return new functions that are transformations of those arguments. These operations permit many kinds of composition of functions and other objects to generate new functions. Three of these functions take predicates as arguments, and return the complement, disjunction, or conjunction of the predicates. For example, complement takes a predicate and returns the latters complement a function that returns #t when the original predicate would have returned #f, and otherwise returns #f. The curry function takes a function and any number of additional arguments. It returns a new function that applies the original function, rst to the additional arguments to curry, then to the arguments to the new function. In Using map and curry, we call curry with * and a number to return a function that multiplies that functions argument by the given number. We then map this new function over the elements of a vector to perform a scalar multiplication of the vector. 4.4. Functions 121
In fact, Dylan has a set of functions that map other functions over the elements of collections in different ways. We used one of these, choose, in Creation of methods. Some of these functions return new collections; others return single values. For more examples, see Iteration over a sequence. Closures This section describes closures an advanced concept. If you do not understand or wish to study this section, you can safely skip it. Consider the following example:
define method call-and-show (function :: <function>, #rest arguments) format-out("The result is %=.\n", apply(function, arguments)); end method call-and-show; define method show-next (x :: <integer>) call-and-show(method () x + 1 end method); end method show-next;
When we execute this code, we get the expected result: ? show-next(41); => The result is 42. But why did we get that result? We created an anonymous method in show-next, and passed that anonymous method into a completely separate method (call-and-show), where x is not bound to anything. And yet, when the call-and-show method executed the anonymous method that we made, somehow the anonymous method could still access the x binding. We got this reasonable result because the method statement can create a special kind of method called a closure. Recall that Dylan has two kinds of variable: module variables and local variables. A local variable is dened explicitly by a let or local declaration, and implicitly by a function call, when a methods parameters are initialized to that methods arguments. Local variables are dened within a limited lexical scope that is, they bind a name to a value only within a particular textual portion of the program. This portion of the program is that part of the innermost body that follows the denition of the local variable. A method statement or a local declaration can dene a method in a portion of a program where local variables are in effect. In the preceding example, we use a method statement to dene a method inside the body of the show-next method, where the local variable x (the parameter for the show-next method) is bound to the argument to show-next. The method that we dene inside show-next refers to that local variable x. In general, when a program exits a body, the local variables dened inside that body cease to be dened, and it is an error for the program to refer to those variables. But there is an exception. If we use method or local to dene a method, and if we then execute that method outside the body in which we dene it, the method can still refer to the local variables that were in effect when the method was dened. Such a method is called a closure. A closure is a method that closes over or captures local variables that are in effect when the method is dened and that are referred to in the body of the method. The closure created by the method statement in our example captures the local variable x. So, even though the local variable x is not dened in the lexical scope of the call-and-show method, the closure called by call-and-show can access the captured binding of x. For examples of closures as iteration or mapping functions for collections, see Mapping functions, and Using map and curry.
4.4.4 Summary
In this chapter, we covered the following:
122
We described the syntax of Dylan function calls, including syntactic structures that are abbreviations for function calls. These syntactic structures include slot references, element references, and most operator calls. We described how a function and its caller interact. In particular, we discussed the relations among arguments, parameters, value declarations, and returned values. We discussed the kinds of parameters that a function can have (required, rest, and keyword). We then outlined the rules for congruent parameter lists and value declarations of a generic function and its methods. We discussed ways of creating generic functions and methods, and of applying a function to arguments. We outlined Dylans operations on functions. We introduced the concept of closures.
4.5.1 Libraries
A Dylan library denes a software component a separately compilable unit that can be either a stand-alone program or a component (library) of some larger program. The elements of the core Dylan language are in a library called
123
dylan. The simplest Dylan program consists of at least two libraries: the original program source in the program library, and the dylan library, which supplies the predened Dylan language elements used by the program library. A simple Dylan component may consist of only a single library the component library. The component library will be used by other libraries. The component library will use denitions from the dylan library (and possibly other components). Hence, when combined with other components into a complete program, the program will consist of several libraries. In each Dylan implementation, a library is associated with implementation-specic export information that is automatically maintained by the compiler. The library export information completely describes whatever implementationspecic information is needed for other software components to use the library. Thus, you can use libraries to deliver components in compiled form, keeping the implementation of the library condential. Comparison with C++ and Modula: Dylan libraries are similar to C++ libraries in that they both are potentially shared components of many programs. Unlike C++ libraries, Dylan libraries include all the information needed to be used by another Dylan library there is no companion header le that must be kept up to date. Dylan libraries are analogous to Modula packages all the information necessary to use a library is contained in the library.
4.5.2 Modules
A library is made up of modules, which hold the denitions of the library. Each module species an independent namespace for Dylan constants and variables. Each module can use denitions from other modules in the same library or in other libraries, and each module can provide denitions to other modules in the same or in other libraries. Each module controls the visibility of the names within a module from outside the module. You can use modules both to do information hiding and to prevent name clashes between constants and variables. Namespaces We mentioned in Variables and constants, that Dylan has module variables and module constants. Every module contains its own set of module variables and constants. Two independent modules a and b might both have variables named *x*. These are two different variables with possibly different values. Within module a, a reference to module variable *x* is a reference to a s variable *x*. Within module b, a reference to module variable *x* is a reference to b s variable *x*. In this sense, a module denes its own namespace. Denitions A module variable or module constant is declared and initialized by a denition. We have already seen that define variable is a denition that establishes a module variable, and define constant is a denition that establishes a module constant. Dylan also uses module constants to refer to classes, generic functions, and macros. The denition for a class, define class, establishes a module constant whose name is the class name and whose value is the class object. Similarly, the denitions for a generic function and a macro establish module constants. When we say that a module contains denitions, we mean that the classes, generic functions, macros, and other objects dened in that module are the values of variables and constants in that module. Export and import of names by modules Within each module, every name refers either to a denition owned by that module, or possibly to a denition owned by another module. Modules make the names of their denitions available to other modules by exporting those names.
124
A module can refer to the names of another module by using the other module. Note that no module can access a denition in another module that is not exported; hence, modules provide a form of access control. When a module exports its names and a second module uses the rst module, importing the names of the rst module, then the denitions of the second module can use the names of the rst module, just as they can use any other name in their own module. When one module uses a second module, it can use all the names exported from the second module, or it can specify a subset of those exports to import. In addition, imported names can be renamed they can be given different names when imported. You can use renaming to document which denitions are from another module, by giving them all a uniform prex; you can use renaming to resolve name conicts; or you can use renaming to give nicknames or shorthand names for imported names. Comparison with C: Exported variables in Dylan are like external variables and functions in C. (By external, we do not mean the extern storage declaration, but rather the concept of an external variable one that is available for linking to.) Unexported variables in Dylan are like static variables and functions in C.
Comparison with C++: Dylan modules are similar to C++ namespaces in that they eliminate the problem of global namespace pollution or clashes between names used in individual libraries. Unlike C++ namespaces, Dylan modules also dene a level of access control: Each module decides what names are externally visible, and no module can create or access names in another module, unless that other module explicitly exports those names. In contrast, the C++ using declaration allows the client of a namespace to access any name in that namespace.
Export and import of modules by libraries Just as a module species a namespace for denitions, each library species an independent namespace of modules and controls the visibility of its modules. Within each library, every module refers either to a module owned by that library, or to a module owned by another library. Libraries make their modules available to other libraries by exporting those modules. A library can refer to the modules of another library by using the other library. No library can refer to the modules of another library that are not exported. When a library exports a module and a second library uses the rst library, importing its modules, then the modules of the second library can use the modules of the rst library, just as they can use any other modules in their own library. When one library uses another library, it can use all the modules exported from the second library, or it can specify a subset of those exports to import. Imported modules can be renamed as they are imported, just as imported module names can be removed. You can see that libraries and modules together provide a two-level structure of naming, information hiding, and access control. The designers of Dylan believed that only a single level would not give sufcient exibility, but that more than two levels was unnecessary. In essence, modules give a ne level of control that lets you organize within a single component, and libraries give a higher level of control that lets you organize components into a program. Also, libraries are the Dylan compilation unit they are the level at which components can be exchanged without source code being exchanged. A software publisher would typically sell its wares as Dylan libraries.
125
Simple example of libraries and modules To illustrate these concepts, we repeat the denition of the library.dylan le, rst shown in Quick Start. Here, we have used a more verbose, but also more precise, format. The library le: library.dylan.
module: dylan-user define library hello use dylan, import: { dylan }; use format-out, import: { format-out }; end library hello; define module hello use dylan, import: all; use format-out, import: all; end module hello;
The rst line of library.dylan states that the expressions and denitions in this le are in the dylan-user module. In this predened module, you dene the modules and library that make up your component or program. Every library has a unique dylan-user module. In the le library.dylan, we dene a library named hello and a module named hello. The module denition names the other modules whose names the hello module will use. In this case, the hello module uses the dylan and format-out modules. Here, we have explicitly stated that we are importing all the names from the modules that we use using the import: all clause is not strictly necessary, because it is the default that is used if we do not specify what to import. By using another module, we import the names exported from that module, making them available in our namespace. For example, format-out is exported from the format-out module, so the use format-out clause enables our program to call the format-out function. The use dylan clause in the module denition makes available all the built-in Dylan language elements exported from the dylan module. When we dene a module, it must use all the modules that export the denitions used by the denitions in our module. The library denition tells the compiler which other libraries our program uses. Here, we have explicitly stated that we are interested in only the dylan and format-out modules from these other libraries. This clause is not strictly necessary, since the module denition tells the compiler which modules it uses; but it is good practice to document our intent. For example, the format-out module is in the format-out library. Therefore, our hello library must use the format-out library, and must import the format-out module for the hello module to use the format-out module. Similarly, the dylan module is in the dylan library, and therefore our hello library must use the dylan library and import the dylan module in order for the hello module to use the dylan module. When we dene a library, it must use all the libraries that export the modules used by the modules in our library. The module denition also species which variables and constants are exported from the module for use by other modules. The library denition species which modules are exported from the library for use by other libraries. In our simple example, the hello module exports no variables or constants, and the hello library exports no modules. Libraries and modules illustrates the relationships between libraries and modules in our example program. In Libraries and modules, and in the other gures in this chapter, we draw libraries as heavy bold boxes and modules as light boxes. We have tried to illustrate how libraries and modules build on one another through their use relationships. A library that uses another library is shown above the library that it uses, so we show the hello library above the format-out and dylan libraries. An exported module is illustrated as being on top of (overlapping) the library that exports it (we have also shaded them, to illustrate this overlap). And a module that uses another module is illustrated as being on top of (overlapping) the used module. Try to envision the modules as semitransparent overlays, layered up from the surface of the paper. Thus, the hello module overlays the format-out and dylan modules that it uses. Note that we intentionally do not show all the modules in the format-out and dylan libraries in Libraries and modules,. The format-out and dylan libraries might well have other modules, but either those modules are not 126 Chapter 4. Part 2. Intermediate Topics
Figure 4.6: Libraries (heavy boxes) and modules (light boxes) in Hello, world. exported or our program does not use them.
127
Modules in other libraries that are not exported, or ones that are exported but are not imported by the modules library, are not visible to the module. Dylan implementations can associate a module with a library in different ways. The library-interchange denition (LID) format lists the interchange les that make up a library. The module denitions in those interchange les are thus in that library. Libraries and programs Every library is in a set of libraries that can be combined into a program; therefore, The library can import the exported modules of any other available library. The librarys exported modules are visible to, and can be imported by, other available libraries. The Dylan implementation determines what libraries are available; how they are combined into a program; and how they are compiled, linked, and run. Consult your implementation documentation for further information. We have presented a simple hierarchical model: All Dylan code resides in source records; every source record resides in a module; every module resides in a library. Every module must be completely dened within its library, because the library is the Dylan unit of compilation. So that this restriction is enforced, every source record in a library must be in a module that is dened in the library; no source record can be in a module that is imported by the library. Within a library, it is possible for a name to be owned by one module and for that names denition to be provided by another module. This exibility helps us to structure code, as we shall see in Module denition.
128
It is possible for a module to play more than one role for example, a client module may also implement a higherlevel interface. We recommend thinking of modules as having these roles, and in this chapter we use that design convention. When illustrating the roles of modules, we use the conventions shown in The roles of modules: interface, implementation, and client.. In The roles of modules: interface, implementation, and client., we show a library with three modules: an interface module (with its interface sticking out of the top of the library), an implementation module (overlapping the interface, because it implements the interface by giving denitions to the names the interface exports), and a client module overlapping another librarys interface module (using its exported interface module to import denitions from another library). As we noted, the implementation and client are often the same module, and the interface of one library is used by the clients of other libraries. Dylan modules and libraries are not allowed to have mutual dependencies, so we can use the convention of drawing at the top the interfaces that a library exports, and of drawing at the bottom the interfaces that a library uses. It is difcult simultaneously to illustrate the module use relationships in only two dimensions the overlapping of one module by another is intended to depict usage.
The interface module We can now write a rst draft of the interface module for our library:
define module time // Classes create <time>, <time-of-day>, <time-offset>; // Generics create say, encode-total-seconds; end module time;
In the preceding denition, the time interface module creates and exports (makes visible) three classes and two functions. We use the create clause, because we do not intend to dene any implementations in the time-library interface module itself that will be done in an implementation module, which will use the time-library module as its interface. The create clause causes the names to be reserved in the time interface module, with the requisite that denitions be provided by some other module in the same library. Comparison with C: The Dylan create clause is roughly analogous to the C extern declaration.
129
The implementation module Our time interface module species the names that are visible to clients of our library. It also serves to specify the names that must be dened in our implementation. To prepare to dene those names, we create a separate implementation module:
define module time-implementation // Interface module use time; // Substrate modules use format-out; use dylan; end module time-implementation;
In the preceding denition, the implementation module uses the time interface module so that it can give denitions to the names that the interface created. The implementation module is also a client module: It is a client of the dylan module, because its denitions use denitions such as define class, <integer>, and * (which are dened by the dylan module of the dylan library); it is also a client of the format-out module, because the say methods are implemented using the format-out function (which is dened in the format-out module of the format-out library). We can start to envision the time library as shown in Initial time library.. In a library more complicated than the time library, we might decompose the construction of the library into several implementation modules. For example, we might want to assign the implementation of the <sixty-unit> substrate to another programmer, and to create an interface between that substrate and the rest of the implementation so that work on either side of the interface can proceed in parallel. In that case, we might use the following module denitions:
define module sixty-unit // External interface use time; // Internal interface export <sixty-unit>, total-seconds, decode-total-seconds; // Substrate module use dylan; end module sixty-unit;
130
use time; // Substrate modules use sixty-unit; use format-out; use dylan; end module time-implementation;
Here, because the sixty-unit module is an internal interface, we forgo the formality of creating a separate implementation module; we simply export the denitions that we expect to be used by other modules within the library. This approach is perhaps a short-sighted one. If later we want the sixty-unit functionality to be available to another library, we will be faced with reorganizing its module denitions (as we shall see in Component library). Even within a library, it is good practice to organize modules as interface and implementation. Notice the distinction between the way that we handled the external time interface, and the shortcut we took with sixty-unit. Although the sixty-unit module will dene encode-total-seconds, which is part of the time interface, it does not export encode-total-seconds; rather, it uses the time interface module, which created encode-total-seconds (without dening that function). Because sixty-unit uses time, the name encode-total-seconds is the same object in both modules. Effectively, encode-total-seconds is owned by the time module, although it is dened by the sixty-unit module. This organization of the external interface may appear odd at rst, but it reduces duplication that would otherwise have to occur: If sixty-unit exported encode-total-seconds, then, for it to be visible at the interface of the library, either the sixty-unit module would have to be exported from the library as an interface (which export is undesirable, because the sixty-unit module has other exports that are not intended to be visible outside the library), or the time interface module would have to use sixty-unit and to re-export encode-total-seconds. The create clause provides the cleaner solution of allowing a name to be exported from only the one interface module, dened in a separate implementation module (without exposing the implementation module), and used by many client modules. Dylan requires that all the variables exported via the create clause be dened by some module in the same library; however, they can be dened in any module, and the interface denitions can be spread over several implementation modules. The compiler will verify that the interface is implemented completely, even if its implementation is spread over several modules, by checking when the library is compiled that each created name has a denition. The sixty-unit module exports the class <sixty-unit>, because time-implementation will subclass that class. The sixty-unit module also exports the generic functions total-seconds, and decode-total-seconds. The export of total-seconds might seem surprising at rst, because, in many object-oriented languages, access to a class includes access to all the slots of a class. In Dylan, slots are simply methods on generic functions and names in the module namespace; hence, the functions must be exported if slot access from outside the module is to be allowed. Note that exporting total-seconds allows other modules only to get the current value of the total-seconds slot. To allow other modules also to set the slot value, we would have to export total-seconds-setter. It is not necessary to export the init keyword total-seconds:, which allows the initial value of the slot to be set when objects are created. Keywords, or symbols, all exist in a single global namespace that is separate from module variables.
131
Comparison with C++: Dylan modules provide access control similar to that provided by the private: and public: keywords in C++ classes, but Dylan access control is done at the module, rather than at the class, level. Dylan has no equivalent to protected: access control, in that a class that subclasses a class from another module does not have access to slots or other generic functions on its superclass from the other module, unless they are explicitly exported from that module. Dylan does support multiple interfaces, however; different levels of access can be provided by having more than one interface module, each supplying the access needed for the particular interface. One way to think of Dylan access control in C++ terms is that all denitions in a module are friends of all classes in the module, and the exported denitions of the module are public. Breaking out the sixty-unit substrate to a separate module creates a slightly more complicated structure to our diagram, as shown in Internal modules of time library..
Figure 4.9: Internal modules of time library. In Internal modules of time library., we show the denitions of sixty-unit in a separate module. The sixty-unit module is a client of dylan, an interface and implementation of denitions used by time-implementation (that is, time-implementation is a client of sixty-unit), and an implementation of part of the interface created by time.
In the preceding denition, we declare that the interface to our library is dened by the time interface module. By exporting that module, we make all the exported names from that module accessible to clients of this library. We also declare that the time library relies on the format-out and dylan libraries (that is, that those libraries have interface
132
modules of which our modules will be clients). Notice that no mention is made of the time-implementation, or sixty-unit modules, because they are completely internal to our library and are not visible to any clients of our library. Recall that constant and variable names, module names, and library names are distinct, so it is possible to have a library, module, and constant all of the same name. A common convention in a library with only one interface module is to give them the same name, as we have done here. To build our library, we would need to dene the library, dene all the modules, specify where and how the denitions or source records that implement our library are to be found, specify where the object code that results from compiling the source records are to be stored, and provide any particular instructions to the compiler regarding how to build the library. The details of how to provide this information vary from one Dylan implementation to the next. To use our library, we would need to specify where to nd the object code and the implementation-dependent export information that allows another library to use our library without access to our source records. The details of this information also depend on the Dylan implementation that we are using. Comparison with C++: The library denition, which names the modules exported and libraries used by a library, is similar to C++ header les and includes. The main difference is that the Dylan development environment extracts the information that it needs about exported and imported variables directly, rather than requiring exports to be duplicated in a set of header les, and requiring those header les to be included in every source le that uses the imports.
133
// Interface module define module time // Classes create <time>, <time-of-day>, <time-offset>; // Generics create say, encode-total-seconds; end module time; // Internal substrate module define module sixty-unit // External interface use time; // Internal interface export <sixty-unit>, total-seconds, decode-total-seconds; // Substrate module use dylan; end module sixty-unit; // Implementation module define module time-implementation // External interface use time; // Substrate modules use sixty-unit; use format-out; use dylan; end module time-implementation;
Because every le has to name the module to which its source records belong, you might wonder where to start. Every library implicitly denes a dylan-user module for this purpose. The dylan-user module imports all of the dylan module, so any Dylan denition can be used. You can think of dylan-user as being a scratch version of dylan. Each library has a private copy of dylan-user, so there is no concern that denitions in one librarys dylan-user could be confused with those of another. The purposes of the library le are to communicate to the Dylan compiler the structure of the module namespaces, to state which other libraries to search for the modules that are used in the implementation of this library, and to determine which modules implemented by this library are visible to other libraries (and programs) that use this library. The details of how these tasks are done depend on the implementation, but each environment will provide a mechanism for reading library and module denitions, either directly from an interchange le, or after conversion of the interchange le to an implementation-dependent format. The sixty-unit implementation le The sixty-unit implementation le: sixty-unit.dylan.
Module: sixty-unit define abstract class <sixty-unit> (<object>) slot total-seconds :: <integer>, required-init-keyword: total-seconds:; end class <sixty-unit>; define method encode-total-seconds (max-unit :: <integer>, minutes :: <integer>, seconds :: <integer>) => (total-seconds :: <integer>) ((max-unit * 60) + minutes) * 60 + seconds; end method encode-total-seconds;
134
define method decode-total-seconds (sixty-unit :: <sixty-unit>) => (max-unit :: <integer>, minutes :: <integer>, seconds :: <integer>) decode-total-seconds(sixty-unit.total-seconds); end method decode-total-seconds; define method decode-total-seconds (total-seconds :: <integer>) => (hours :: <integer>, minutes :: <integer>, seconds :: <integer>) let (total-minutes, seconds) = truncate/(abs(total-seconds), 60); let (hours, minutes) = truncate/(total-minutes, 60); values(hours, minutes, seconds); end method decode-total-seconds;
The preceding implementation le is the rst le in which we use one of our own modules. The header statement Module: sixty-unit tells the Dylan compiler where to look to resolve the names that we are using it tells Dylan that, when we say define class or <integer> or *, we mean the Dylan denitions of define class, <integer>, and *, because sixty-unit uses the dylan module. When we dene encode-total-seconds, we mean the encode-total-seconds created by the time module, because sixty-unit uses that module. The time implementation le The time implementation le: time.dylan.
Module: time-implementation // Define nonnegative integers as integers that are >= zero define constant <nonnegative-integer> = limited(<integer>, min: 0); define abstract class <time> (<sixty-unit>) end class <time>; define method say (time :: <time>) => () let (hours, minutes) = decode-total-seconds(time); format-out("%d:%s%d", hours, if (minutes < 10) "0" else " " end, minutes); end method say; // A specific time of day from 00:00 (midnight) to before 24:00 (tomorrow) define class <time-of-day> (<time>) end class <time-of-day>; define method total-seconds-setter (total-seconds :: <integer>, time :: <time-of-day>) => (total-seconds :: <nonnegative-integer>) if (total-seconds >= 0) next-method(); else error("%d cannot be negative", total-seconds); end if; end method total-seconds-setter; define method initialize (time :: <time-of-day>, #key) next-method(); if (time.total-seconds < 0) error("%d cannot be negative", time.total-seconds); end if;
135
end method initialize; // A relative time between -24:00 and +24:00 define class <time-offset> (<time>) end class <time-offset>; define method past? (time :: <time-offset>) => (past? :: <boolean>) time.total-seconds < 0; end method past?; define method say (time :: <time-offset>) => () format-out("%s ", if (time.past?) "minus" else "plus" end); next-method(); end method say; define method \+ (offset1 :: <time-offset>, offset2 :: <time-offset>) => (sum :: <time-offset>) let sum = offset1.total-seconds + offset2.total-seconds; make(<time-offset>, total-seconds: sum); end method \+; define method \+ (offset :: <time-offset>, time-of-day :: <time-of-day>) => (sum :: <time-of-day>) make(<time-of-day>, total-seconds: offset.total-seconds + time-of-day.total-seconds); end method \+; define method \+ (time-of-day :: <time-of-day>, offset :: <time-offset>) => (sum :: <time-of-day>) offset + time-of-day; end method \+; define method \< (time1 :: <time-of-day>, time2 :: <time-of-day>) time1.total-seconds < time2.total-seconds; end method \<; define method \< (time1 :: <time-offset>, time2 :: <time-offset>) time1.total-seconds < time2.total-seconds; end method \<; define method \= (time1 :: <time-of-day>, time2 :: <time-of-day>) time1.total-seconds = time2.total-seconds; end method \=; define method \= (time1 :: <time-offset>, time2 :: <time-offset>) time1.total-seconds = time2.total-seconds; end method \=; // Two useful time constants define constant $midnight = make(<time-of-day>, total-seconds: encode-total-seconds(0, 0, 0)); define constant $tomorrow = make(<time-of-day>, total-seconds: encode-total-seconds(24, 0, 0));
136
In the preceding implementation le, it is the time-implementation module that species what we mean when we write Dylan expressions, and in which module namespace our denitions will appear. The library-interchange denition (LID) As described in Files of a Dylan program, most Dylan implementations also accept a LID le that enumerates the les of a library and the order in which those les will be initialized, if there are any top-level forms. The LID le for our time library would be as follows. The LID le: time.lid.
library: time files: library sixty-unit time
In a LID le, only the base le name is given. Information about the folder or directory where the les are stored, and about the le extension (.dylan in our examples), is implementation dependent and must be supplied by the individual implementation.
137
create total-seconds, encode-total-seconds, decode-total-seconds; end module sixty-unit; // Implementation module define module sixty-unit-implementation // External interface use sixty-unit; // Substrate module use dylan; end module sixty-unit-implementation;
Notice that we have taken this opportunity to reorganize the sixty-unit module into a separate interface and implementation. We also have to create encode-total-seconds in the sixty-unit module, rather than to create it in the time interface and to dene it in sixty-unit. Recall that all created names must be dened in the library in which they are created; we cannot use the createdene structure across libraries. We still want encode-total-seconds to be part of the interface of the time library, so we will have to change the time interface module to import it and to re-export it from the time library, as shown in The updated time-library le. If we had followed our own recommendations in The implementation module, we would probably have discovered that encode-total-seconds belonged in the sixty-unit interface, and we would have avoided most of this reorganization. The updated time-library le The time-library le: time-library.dylan.
Module: dylan-user // Library definition define library time // Interface module export time; // Substrate libraries use sixty-unit; use format-out; use dylan; end library time; // Interface module define module time // Classes create <time>, <time-of-day>, <time-offset>; // Generics create say; // Shared protocol use sixty-unit, import: { encode-total-seconds }, export: all; end module time; // Implementation module define module time-implementation // External interface use time; // Substrate modules use sixty-unit; use format-out; use dylan; end module time-implementation;
138
Note that the time interface module imports only encode-total-seconds from sixty-unit. It then reexports all the names that it has imported in this case, just encode-total-seconds. In this way, the time interface is acting as a lter and is passing on only a subset of the sixty-unit interface to its clients. At this point, we need to open the <sixty-unit> class. Because it is now in a separate library, it must be dened to be open to allow other libraries, such as time or angle, to subclass it. Opening a class simply amounts to changing the define class to define open class. The exact implications of this declaration are discussed in Performance and Flexibility. The updated sixty-unit implementation le The sixty-unit implementation le: sixty-unit.dylan.
Module: sixty-unit-implementation define open abstract class <sixty-unit> (<object>) slot total-seconds :: <integer>, required-init-keyword: total-seconds:; end class <sixty-unit>; define method encode-total-seconds (max-unit :: <integer>, minutes :: <integer>, seconds :: <integer>) => (total-seconds :: <integer>) ((max-unit * 60) + minutes) * 60 + seconds; end method encode-total-seconds; define method decode-total-seconds (sixty-unit :: <sixty-unit>) => (max-unit :: <integer>, minutes :: <integer>, seconds :: <integer>) decode-total-seconds(sixty-unit.total-seconds); end method decode-total-seconds; define method decode-total-seconds (total-seconds :: <integer>) => (hours :: <integer>, minutes :: <integer>, seconds :: <integer>) let (total-minutes, seconds) = truncate/(abs(total-seconds), 60); let (hours, minutes) = truncate/(total-minutes, 60); values(hours, minutes, seconds); end method decode-total-seconds;
sixty-unit as a separate library. shows the relationships among our libraries and modules at this point. Note that sixty-unit is now a separate library. It uses the dylan library and is used by the time library. We illustrate the time module importing and re-exporting part of the sixty-unit interface module (the method encode-total-seconds) by the darker grey area. Two LID les Here, we show the LID les for each library. The LID le: sixty-unit.lid.
library: sixty-unit files: sixty-unit-library sixty-unit
139
140
// Library definition define library say // Interface modules export say, say-implementor; // Substrate libraries use format-out; use dylan; end library say; // Protocol interface define module say create say; end module say; // Implementor interface define module say-implementor use say, export: all; use format-out, export: all; end module say-implementor; // Implementation module define module say-implementation use say; use dylan; end module say-implementation;
Here, we have created the recommended interface and implementation structure, having learned our lesson with the sixty-unit module. Even though it looks like overkill to have a separate implementation module for a single generic function denition, we have planned for future expansion. The say protocol library is an example of the multiple-interface capability of Dylan libraries. The say library has two interfaces that it makes available: say denes the say protocol, and say-implementor provides the substrate for protocol implementors. This interface is cleaner than the one that we used for sixty-unit, where encode-total-seconds played more of an interface role, and <sixty-unit> and decode-total-seconds played more of a substrate role. The result is seen in the clients of the sixty-unit library, who must split out these roles for themselves. Note that the say-implementor module is both a client and an interface module. It is the interface of the say protocol for clients who will implement say methods, and it is a client of the format-out module. Because most say methods use format-out in their implementations, it makes sense to re-export all of the format-out module for say-implementor clients. The explicit denition of the say generic function is good protocol documentation. It is also required: All module variables must have a denition for a library to be complete. (An alternative would have been to dene a default method for say, which would also create an implicit generic-function denition. However, implicit generic-function denitions are sealed, and, for a protocol, we need an open generic function, because we intend clients to add methods to it. The exact implications of this declaration are discussed in Performance and Flexibility.) The designer of the say protocol still has to choose whether to require each type to dene its own say method, or to provide a universal
141
default. In this case, we choose not to provide a default, so that an error will be signaled if say is called on a type that does not either provide or inherit a say method. Comparison with C++: Dylan modules enforce a structured design of protocols. To create a shared protocol, to which methods can be added from independent libraries, we must ensure that the module dening the protocol (the module dening the generic function) is dened rst, in a separate, common library. The common library denes the protocol in one place, easing documentation and maintenance. In C++ however, a using directive can create a local alias to overload a function in any other library, even if it is in another namespace. The library-use relationships of Dylan modules form a directed graph, centralizing shared functionality, whereas C++ namespaces can be interconnected arbitrarily, making documentation and maintenance of shared protocols difcult. To complete our restructuring, we must reorganize the time library and module les to use the say protocol, so that the say protocol is shared with the angle library that we intend to build. The updated time-library le The time-library le: time-library.dylan.
Module: dylan-user // Library definition define library time // Interface module export time; // Substrate libraries use sixty-unit; use say; use dylan; end library time; // Interface module define module time // Classes create <time>, <time-of-day>, <time-offset>; // Shared protocol use say, export: all; use sixty-unit, import: { encode-total-seconds }, export: all; end module time; // Implementation module define module time-implementation // External interface use time; // Substrate modules use sixty-unit; use say-implementor; use dylan; end module time-implementation;
The time module is modied to use say, which it exports to its clients. The implementation module is modied to use say-implementor, which includes format-out, so it would be superuous to continue to include format-out in time-implementation. Similarly, the time library denition replaces its use of the 142 Chapter 4. Part 2. Intermediate Topics
format-out library with the say library. Note that the compiler recursively nds all the libraries necessary for compilation. In this case, the format-out library will be included in the compilation of the time library, even though it is not directly named. The angle library At this point, we are ready to dene the angle library, which will share the sixty-unit and say libraries with the time library. In Four Complete Libraries, we present the consolidated changes to the sixty-unit, say, and time libraries that we have developed in this chapter, followed by the complete denition of the angle library.
4.5.10 Summary
In this chapter, we covered the following: We illustrated Dylan modules and libraries. We showed how to design modules using three roles: interface modules, implementation modules, and client modules. We described how a library might appear in Dylan interchange format. We showed how to create a component library. We illustrated the complexity of component and protocol design. We discussed how to create a protocol that can be extended by multiple client libraries. We discussed namespaces in Dylan, and their applicable scope; see Namespace scopes.. We described the roles of modules and the denition clauses that modules use; see Module roles.. Table 4.4: Namespace scopes. Namespace library module constant or variable symbol or keyword Scope global per library per module global
143
Table 4.5: Module roles. Role interface Example clause // Interface class create <time>; // Re-exported interface use say, export: all; // Substrate module use dylan; // Interface module use time; // Interface protocol export say;
client
implementation
144
create total-seconds, encode-total-seconds, decode-total-seconds; end module sixty-unit; // Implementation module define module sixty-unit-implementation // External interface use sixty-unit; // Substrate module use dylan; end module sixty-unit-implementation;
145
The say library comprises two Dylan interchange-format les: a library le, containing the library and module denitions; and an implementation le, containing a single source record, dening the generic function that is the say protocol. For completeness, we also show the LID le that describes the library and its component les. The say-library le The say-library le: say-library.dylan.
Module: dylan-user // Library definition define library say // Interface modules export say, say-implementor; // Substrate libraries use format-out; use dylan; end library say; // Protocol interface define module say create say; end module say; // Implementor interface define module say-implementor use say, export: all; use format-out, export: all; end module say-implementor; // Implementation module define module say-implementation use say; use dylan; end module say-implementation;
146
147
define method say (time :: <time>) => () let (hours, minutes) = decode-total-seconds(time); format-out("%d:%s%d", hours, if (minutes < 10) "0" else " " end, minutes); end method say; // A specific time of day from 00:00 (midnight) to before 24:00 (tomorrow) define class <time-of-day> (<time>) end class <time-of-day>; define method total-seconds-setter (total-seconds :: <integer>, time :: <time-of-day>) => (total-seconds :: <nonnegative-integer>) if (total-seconds >= 0) next-method(); else error("%d cannot be negative", total-seconds); end if; end method total-seconds-setter; define method initialize (time :: <time-of-day>, #key) next-method(); if (time.total-seconds < 0) error("%d cannot be negative", time.total-seconds); end if; end method initialize; // A relative time between -24:00 and +24:00 define class <time-offset> (<time>) end class <time-offset>; define method past? (time :: <time-offset>) => (past? :: <boolean>) time.total-seconds < 0; end method past?; define method say (time :: <time-offset>) => () format-out("%s ", if (time.past?) "minus" else "plus" end); next-method(); end method say; define method \+ (offset1 :: <time-offset>, offset2 :: <time-offset>) => (sum :: <time-offset>) let sum = offset1.total-seconds + offset2.total-seconds; make(<time-offset>, total-seconds: sum); end method \+; define method \+ (offset :: <time-offset>, time-of-day :: <time-of-day>) => (sum :: <time-of-day>) make(<time-of-day>, total-seconds: offset.total-seconds + time-of-day.total-seconds); end method \+; define method \+ (time-of-day :: <time-of-day>, offset :: <time-offset>) => (sum :: <time-of-day>) offset + time-of-day; end method \+;
148
define method \< (time1 :: <time-of-day>, time2 :: <time-of-day>) time1.total-seconds < time2.total-seconds; end method \<; define method \< (time1 :: <time-offset>, time2 :: <time-offset>) time1.total-seconds < time2.total-seconds; end method \<; define method \= (time1 :: <time-of-day>, time2 :: <time-of-day>) time1.total-seconds = time2.total-seconds; end method \=; define method \= (time1 :: <time-offset>, time2 :: <time-offset>) time1.total-seconds = time2.total-seconds; end method \=; // Two useful time constants define constant $midnight = make(<time-of-day>, total-seconds: encode-total-seconds(0, 0, 0)); define constant $tomorrow = make(<time-of-day>, total-seconds: encode-total-seconds(24, 0, 0));
149
internal-direction slot is truly internal to the angle library no client library can even determine its existence. The angle-library le The angle-library le: angle-library.dylan.
Module: dylan-user // Library definition define library angle // Interface module export angle, position; // Substrate libraries use sixty-unit; use say; use dylan; end library angle; // Interface module define module angle // Classes create <angle>, <relative-angle>, <directed-angle>, <latitude>, <longitude>; // Generics create direction, direction-setter; // Shared protocol use say, export: all; use sixty-unit, import: { encode-total-seconds }, export: all; end module angle; // Interface module define module position // Classes create <position>, <absolute-position>, <relative-position>; // Generics create distance, angle, latitude, longitude; // Shared protocol use say, export: all; end module position; // Implementation module define module angle-implementation // External interface use angle; // Substrate modules use sixty-unit; use say-implementor; use dylan; end module angle-implementation; // Implementation module define module position-implementation // External interface use position; // Substrate modules use angle; use say-implementor;
150
The angle implementation le The angle implementation le is simply a collection of the source records that we developed earlier for creating and saying angles, latitudes, and longitudes. The angle implementation le: angle.dylan.
Module: angle-implementation define abstract class <angle> (<sixty-unit>) end class <angle>; define method say (angle :: <angle>) => () let (degrees, minutes, seconds) = decode-total-seconds(angle); format-out("%d degrees %d minutes %d seconds", degrees, minutes, seconds); end method say; define class <relative-angle> (<angle>) end class <relative-angle>; define method say (angle :: <relative-angle>) => () format-out(" %d degrees", decode-total-seconds(angle)); end method say; define abstract class <directed-angle> (<angle>) virtual slot direction :: <symbol>; slot internal-direction :: <symbol>; keyword direction:; end class <directed-angle>; define method initialize (angle :: <directed-angle>, #key direction: dir) next-method(); angle.direction := dir; end method initialize; define method direction (angle :: <directed-angle>) => (dir :: <symbol>) angle.internal-direction; end method direction; define method direction-setter (dir :: <symbol>, angle :: <directed-angle>) => (new-dir :: <symbol>) angle.internal-direction := dir; end method direction-setter; define method say (angle :: <directed-angle>) => () next-method(); format-out(" %s", angle.direction); end method say; define class <latitude> (<directed-angle>) end class <latitude>; define method say (latitude :: <latitude>) => ()
151
next-method(); format-out(" latitude\n"); end method say; define method direction-setter (dir :: <symbol>, latitude :: <latitude>) => (new-dir :: <symbol>) if (dir == #"north" | dir == #"south") next-method(); else error("%= is not north or south", dir); end if; end method direction-setter; define class <longitude> (<directed-angle>) end class <longitude>; define method say (longitude :: <longitude>) => () next-method(); format-out(" longitude\n"); end method say; define method direction-setter (dir :: <symbol>, longitude :: <longitude>) => (new-dir :: <symbol>) if (dir == #"east" | dir == #"west") next-method(); else error("%= is not east or west", dir); end if; end method direction-setter;
The position implementation le The position implementation le is simply a collection of the source records that we developed earlier for creating and saying absolute and relative positions. The position implementation le: position.dylan.
Module: position-implementation define abstract class <position> (<object>) end class <position>; define class <absolute-position> (<position>) slot latitude :: <latitude>, required-init-keyword: latitude:; slot longitude :: <longitude>, required-init-keyword: longitude:; end class <absolute-position>; define method say (position :: <absolute-position>) => () say(position.latitude); say(position.longitude); end method say; define class <relative-position> (<position>) // Distance is in miles slot distance :: <single-float>, required-init-keyword: distance:; // Angle is in degrees slot angle :: <angle>, required-init-keyword: angle:;
152
end class <relative-position>; define method say (position :: <relative-position>) => () format-out("%s miles away at heading ", position.distance); say(position.angle); end method say;
The angle LID le Because we have chosen to put the source records for positions in a separate interchange le, the LID le lists three Dylan les that make up the angle library. The LID le: angle.lid.
library: angle files: angle-library angle position
4.6.5 Summary
The structure of protocol and substrate libraries that we have created is perhaps overly complex for the simple functionality that we have implemented here. However, the libraries illustrate the power of the Dylan module and library system to modularize large projects into easily manageable sub-projects, and to control the interfaces among those projects. Nonclass Types, discusses types that are not classes, including singleton types, limited types, and union types. Slots, focuses on slot getters and setters, techniques for initializing slots, different kinds of allocation of slots, virtual slots, and symbols. Collections and Control Flow, describes how to use collections, including strings, lists, vectors, tables, and arrays. It also shows how to use control-ow operators to alter the natural (sequential) order of statement execution, including performing iteration. Functions, describes the syntax of function calls, the function-calling protocol, and the uses of functions as objects. Libraries and Modules, shows how you can package your code into a reusable software component by designing libraries and modules. Four Complete Libraries, pulls together the techniques shown in Part 2. Intermediate Topics in the context of a set of complete working libraries.
153
154
CHAPTER
FIVE
Figure 5.2: Transitions between sky and gate for outbound and inbound aircraft. In Objects in a simple airport., we see a single terminal, A. It has two gates, A1 and A2, a taxiway, Echo, a runway, 11R-29L, and an aircraft approaching the runway. 155
When landing, an aircraft goes from the sky to a runway to a taxiway to a gate. Transitions between sky and gate for outbound and inbound aircraft. is a state diagram showing these transitions for both inbound and outbound aircraft. Our rst goal for this application is as follows: given a set of incoming aircraft at various positions in the sky, we want to predict which gate each aircraft will use when it arrives, and to estimate the arrival time of the aircraft at the gate. This information is displayed on the Arrivals monitors in an airport. Our second goal for the application is to provide additional information for the ground crew. We must state the entire path that an incoming aircraft will take, including the runway, the taxiway, and the gate. We must also state the time that an aircraft is expected to be at each point. For example, for an inbound aircraft, we want to display information like the following:
12:30: Aircraft Cardinal at Runway 11R-29L 12:43: Aircraft Cardinal at Taxiway Echo 12:47: Aircraft Cardinal at Gate A2
156
Figure 5.3: Inheritance relationships among classes that represent physical objects.
1. This generic function simulates and documents the movement of aircraft through the airport, including the time of each transition for example,
12:30: Aircraft Cardinal at Runway 11R-29L 12:43: Aircraft Cardinal at Taxiway Echo 12:47: Aircraft Cardinal at Gate A2
157
1. This generic function returns the distance between its two arguments. <aircraft>, and the airport is an instance of <airport>. ying-time aircraft airport
Generic Function
1. This generic function returns the time that it would take for the aircraft to y to the airport.
1. This generic function returns true if there is space in container for vehicle to enter for traveling in direction. move-in-vehicle vehicle container direction
Generic Function
1. This generic function moves the vehicle into the container in the direction given. move-out-vehicle vehicle container direction
Generic Function
1. This generic function moves the vehicle out of the container in the direction given. next-out container direction
Generic Function
1. This generic function determines what vehicle, if any, could move to the next container. If there is such a vehicle, then next-out returns the vehicle, the next container in the direction of travel, and the time it would take to make that transition.
158
1. This generic function returns two values. The rst value is the class of the next container into which vehicle may move from container. The second value is how long it will take vehicle to move into the next container. This generic function is used by the next-out generic function to accomplish part of the latters work.
5.1.6 Testing
We include in the application a test library, which creates instances of the classes described in this chapter, initializes these instances to a reasonable state, and calls process-aircraft. Providing test cases (in a separate library) is a good way to check the design, interface, and implementation of an application library.
5.1.7 Summary
In this chapter, we covered the following: We discussed the goals of the airport application: to predict the arrival time and gate of an incoming aircraft, and to describe the entire path that an incoming aircraft will take, including the time it is expected to be at each point. We discussed the design of the airport application classes, and the operations to be performed on instances of the classes, including the vehicle- container protocol. We discussed how the time, angle, sorted sequence, and testing libraries interact with the main airport application library. In Denition of a New Collection, we implement sorted sequences. In The Airport Application, we implement the airport application.
159
Because is there is a well-dened ordering of the elements of sorted sequences, we choose <sequence> to be the superclass of <sorted-sequence>. We use the built-in collection class called <stretchy-vector> to store the elements of our sorted sequence, because we want to be able to have the sorted sequence grow to any size in a convenient way. The slots comparison-function and value-function are constant slots, because we intend to have clients specify these functions only when they create the sorted sequence. If we had decided to let clients change the value of these slots, we would have made the slots virtual, so that we could reorder the data vector after either function had changed. Now that we have covered the structure and initialization of the sorted sequence data structure, we can dene basic collection methods.
160
In the preceding code, we dene methods for determining the number of elements in the sorted sequence, for copying the sorted sequence (but not the elements stored in the sorted sequence), and for accessing a particular item in the sorted sequence. Once we have dened the element method for sorted sequences, we can use the subscripting syntax to access particular items in the sorted sequence. Our element method implements the standard Dylan protocol, which allows the caller to specify a default value if the key is not contained within the collection. If the key is not part of the collection, and no default value is specied, then an error is signaled. Since we do not export $unsupplied from our library, we can be certain that no one can supply that value as the default keyword parameter for our element method. Note that the element-setter method is not dened, because it does not make sense to store an element at a particular position within the sorted sequence. The sorted sequence itself determines the correct key for each item added to the sorted sequence, based on the item being added and on the value and comparison functions. Next, we show methods for adding and removing elements from sorted sequences. Adding and removing elements The sorted-sequence.dylan le. (continued)
// Add an element to the sorted sequence define method add!
161
(sorted-sequence :: <sorted-sequence>, new-element :: <object>) => (sorted-sequence :: <sorted-sequence>) let element-value = sorted-sequence.value-function; let compare = sorted-sequence.comparison-function; add!(sorted-sequence.data, new-element); sorted-sequence.data := sort!(sorted-sequence.data, test: method (e1, e2) compare(element-value(e1), element-value(e2)) end); sorted-sequence; end method add!; // Remove the item at the top of the sorted sequence define method pop (sorted-sequence :: <sorted-sequence>) => (top-of-sorted-sequence :: <object>) let data-vector = sorted-sequence.data; let top-of-sorted-sequence = data-vector[0]; let sorted-sequence-size = data-vector.size; if (empty?(sorted-sequence)) error("Trying to pop empty sorted-sequence %=.", sorted-sequence); else // Shuffle up existing data, removing the top element from the // sorted sequence for (i from 0 below sorted-sequence-size - 1) data-vector[i] := data-vector[i + 1]; end for; // Decrease the size of the data vector, and return the top element data-vector.size := sorted-sequence-size - 1; top-of-sorted-sequence; end if; end method pop; // Remove a particular element from the sorted sequence define method remove! (sorted-sequence :: <sorted-sequence>, value :: <object>, #key test = \==, count = #f) => (sorted-sequence :: <sorted-sequence>) let data-vector = sorted-sequence.data; let sorted-sequence-size = data-vector.size; for (deletion-point from 0, // If we have reached the end of the sequence, or we have reached // the user-specified limit, we are done // Note that specifying a bound in the preceding clause for // deletion-point does not work, because bounds are computed only // once, and we change sorted-sequence-size in the body until: (deletion-point >= sorted-sequence-size) | (count & count = 0)) // Otherwise, if we found a matching element, remove it from the // sorted sequence. if (test(data-vector[deletion-point], value)) for (i from deletion-point below sorted-sequence-size - 1) data-vector[i] := data-vector[i + 1] end for; sorted-sequence-size := (data-vector.size := sorted-sequence-size - 1); if (count) count := count - 1 end; end if;
162
The remove! method uses a form of the for loop that includes an until: clause, much like the my-copy-sequence method dened in Lists and efciency. Note that all termination checks are tested prior to the execution of the body. Although the pop method is not used in the airport application, it is included for completeness. We could make the pop method faster by storing the data elements in reverse order; however, that would lead to either odd behavior or odd implementation of the element function on sorted sequences. The forward-iteration protocol Dylans forward-iteration protocol allows us to connect the usual collection iteration functions to our new collection class. Connecting to the forward-iteration protocol is as simple as dening an appropriate method for the forward-iteration-protocol generic function. This method must return two objects and six functions. The sorted-sequence.dylan le. (continued)
// This method enables many standard and user-defined collection operations define method forward-iteration-protocol (sorted-sequence :: <sorted-sequence>) => (initial-state :: <integer>, limit :: <integer>, next-state :: <function>, finished-state? :: <function>, current-key :: <function>, current-element :: <function>, current-element-setter :: <function>, copy-state :: <function>) values( // Initial state 0, // Limit sorted-sequence.size, // Next state method (collection :: <sorted-sequence>, state :: <integer>) state + 1 end, // Finished state? method (collection :: <sorted-sequence>, state :: <integer>, limit :: <integer>) state = limit; end, // Current key method (collection :: <sorted-sequence>, state :: <integer>) state end, // Current element element, // Current element setter method (value :: <object>, collection :: <sorted-sequence>, state :: <integer>) error("Setting an element of a sorted sequence is not allowed."); end,
163
If we are to iterate over any collection, we must maintain some state to help the iterator remember the current point of iteration. For the forward-iteration protocol, we maintain this state using any object suitable for a given collection. In this case, an integer is sufcient to maintain where we are in the iteration process. The rst object returned by forward-iteration-protocol is a state object that is suitable for the start of an iteration. The second object returned is a state object that represents the ending state of the iteration. Since, in this case, the state object is just the current key of the sorted sequence, the integer 0 is the correct initial state, and the integer that represents the size of the collection is the correct ending state. The third value returned is a function that takes the collection and the current iteration state, and returns a state that is the next step in the iteration. In this case, we can determine the next state simply by adding 1 to the current state. The fourth value returned is a function that receives the collection, the current state, and the ending state, and that determines whether the iteration is complete. In this case, we need only to check whether the current state is equal to the ending state. The fth value returned is a function that generates the current key into the collection, given a collection and a state. In this case, the key is the state object. The sixth value returned is a function that receives a collection and a state, and returns the current element of the collection. In this case, the element function is the obvious choice, since our state is just the key. The seventh value returned is a function that receives a new value, a collection, and a state, and changes the current element to be the new value. In this case, such an operation is illegal, since the only rational way to add elements to sorted sequences is with add!. Because this operation is illegal, an error is signaled. The eighth and nal value returned is a function that receives a collection and a state, and returns a copy of the state. In this case, we just return the state, because it is an integer and thus has no slots that are modied during the iteration process. If we represented the state with an object that had one or more slots that did change during iteration, we would have to make a new state instance and to copy the signicant information from the old state instance to the new state instance. Once we have dened a forward-iteration-protocol method for sorted sequences, we can iterate over them using for loops, mapping functions, and other collections iterators described in Collections and Control Flow. Also, if someone denes a new iterator that uses the forward-iteration protocol, then this new iterator will work with sorted sequences. Dylan has several other related protocols for backward iteration and for tables. See the The Dylan Reference Manual for details.
164
define module sorted-sequence export <sorted-sequence>; use dylan; use definitions; end module sorted-sequence;
The definitions library and module are dened in The Airport Application.
5.2.4 Summary
In this chapter, we covered the following: We explored how to dene our own collection class. We showed how to integrate that class into Dylans collection framework. We used several variations of the control structures presented in Collections and Control Flow.
165
define constant $hours-per-day = 24; define constant $minutes-per-hour = 60; define constant $seconds-per-minute = 60; define constant $seconds-per-hour = $minutes-per-hour * $seconds-per-minute; // This method returns the union of the false type and a type you specify, // as a simple shorthand // This method may already be provided by your Dylan implementation define method false-or (other-type :: <type>) => (combined-type :: <type>) type-union(singleton(#f), other-type); end method false-or;
In the preceding portion of the airport-classes.dylan le, we dene the class <size>, which allows us to specify the external dimensions and container volume of various objects. For example, we might want to specify that certain gate areas might be too small to hold the large aircraft. We also dene the base class for all tangible objects, <physical-object>. Next, we dene the classes where aircraft are normally located.
166
167
// direction in a different table define method initialize (object :: <multiple-storage>, #key directions :: <sequence>, maxima :: <sequence>) next-method (); for (direction in directions, maximum in maxima) object.vehicles-by-direction[direction] := make(<deque>); object.maxima-by-direction[direction] := maximum; end for; end method initialize; // From the preceding basic vehicle containers, we can build specific // containers for each aircraft-transition location define class <gate> (<single-storage>) inherited slot name-prefix, init-value: "Gate"; end class <gate>; // Given a zero-based terminal number, and a one-based gate number, create // an return a string with a gate letter and a terminal number in it define method generate-gate-id (term :: <nonnegative-integer>, gate :: <positive-integer>) => (gate-id :: <string>) format-to-string("%c%d", $letters[term], gate); end method generate-gate-id; // Gates-per-terminal is a vector; each element of the vector is the // number of gates to create for the terminal at that index // Returns a vector of all the gate instances define method generate-gates (gates-per-terminal :: <vector>, default-gate-capacity :: <size>) => (gates :: <vector>) let result = make(<vector>, size: reduce1(\+, gates-per-terminal)); let result-index = 0; for (term from 0 below gates-per-terminal.size) for (gate from 1 to gates-per-terminal[term]) result[result-index] := make(<gate>, id: generate-gate-id(term, gate), capacity: default-gate-capacity); result-index := result-index + 1; end for; end for; result; end method generate-gates; // This class represents the part of the airspace over a given airport define class <sky> (<multiple-storage>) // The airport over which this piece of sky is located slot airport-below :: <airport>, required-init-keyword: airport:; inherited slot name-prefix, init-value: "Sky"; required keyword inbound-aircraft:; end class <sky>; // // // // // When a sky instance is created, a sequence of inbound aircraft is provided This method initializes the direction slot of the aircraft to #"inbound", and places the aircraft in the inbound queue of the sky instance
168
define method initialize (sky :: <sky>, #key inbound-aircraft :: <sequence>) next-method(sky, directions: #[#"inbound", #"outbound"], maxima: vector(inbound-aircraft.size, inbound-aircraft.size)); let inbound-queue = sky.vehicles-by-direction [#"inbound"]; for (vehicle in inbound-aircraft) vehicle.direction := #"inbound"; push-last(inbound-queue, vehicle); end for; // Connect the airport to the sky sky.airport-below.sky-above := sky; end method initialize; // This class represents a strip of land where aircraft land and take off define class <runway> (<single-storage>) inherited slot name-prefix, init-value: "Runway"; end class <runway>; // Taxiways connect runways and gates define class <taxiway> (<multiple-storage>) inherited slot name-prefix, init-value: "Taxiway"; end class <taxiway>;
In the preceding portion of the airport-classes.dylan le, we dene the tangible objects that represent the various normal locations for aircraft in and around an airport. These locations are known as containers or vehicle storage. We can connect vehicle-storage instances to one another to form an airport. Instances of <single-storage> can hold only one aircraft at a time, whereas instances of <multiple-storage> can hold more than one aircraft at a time. Also, instances of <multiple-storage> treat inbound aircraft separately from outbound aircraft. We dene the object-fits? method, which determines whether a physical object can t into a container. We also dene methods for creating, initializing, and describing various containers. Note the use of the each-subclass slot name-prefix, which permits one say method on the <vehicle-storage> class to cover all the vehiclecontainer classes. Each subclass of vehicle storage can override the inherited value of this slot, to ensure that the proper name of the vehicle storage is used in the description of instances of that subclass. The <vehicle-storage>, <multiple-storage>, and <single-storage> classes are all abstract, because it is not sensible to instantiate them. They contain partial implementations that they contribute to their subclasses. In the generate-gates method, the gates-per-terminal parameter is a vector that contains the count of gates for each terminal. By adding up all the elements of that vector with reduce1, we can compute the total number of gates at the airport, and thus the size of the vector that can hold all the gates. Next, we examine the classes, initialization methods, and say methods for the vehicles in the application. Vehicles The airport-classes.dylan le. (continued)
// VEHICLES // The class that represents all self-propelled devices define abstract class <vehicle> (<physical-object>) // Every vehicle has a unique identification code slot vehicle-id :: <string>, required-init-keyword: id:; // The normal operating speed of this class of vehicle in miles per hour each-subclass slot cruising-speed :: <positive-integer>;
169
// Allow individual differences in the size of particular aircraft, // while providing a suitable default for each class of aircraft each-subclass slot standard-size :: <size>; end class <vehicle>; define method initialize (vehicle :: <vehicle>, #key) next-method(); unless (slot-initialized?(vehicle, physical-size)) vehicle.physical-size := vehicle.standard-size; end unless; end method initialize; define method say (object :: <vehicle>) => () format-out("Vehicle %s", object.vehicle-id); end method say; // This class represents companies that fly commercial aircraft define class <airline> (<object>) slot name :: <string>, required-init-keyword: name:; slot code :: <string>, required-init-keyword: code:; end class <airline>; define method say (object :: <airline>) => () format-out("Airline %s", object.name); end method say; // This class represents a regularly scheduled trip for a commercial // airline define class <flight> (<object>) slot airline :: <airline>, required-init-keyword: airline:; slot number :: <nonnegative-integer>, required-init-keyword: number:; end class <flight>; define method say (object :: <flight>) => () format-out("Flight %s %d", object.airline.code, object.number); end method say; // This class represents vehicles that normally fly for a portion of // their trip define abstract class <aircraft> (<vehicle>) slot altitude :: <integer>, init-keyword: altitude:; // Direction here is either #"inbound" or #"outbound" slot direction :: <symbol>; // The next step this aircraft might be able to make slot next-transition :: <aircraft-transition>, required-init-keyword: transition:, setter: #f; end class <aircraft>; define method initialize (vehicle :: <aircraft>, #key) next-method(); // There is a one-to-one correspondence between aircraft instances and // transition instances // An aircraft can only make one transition at a time // Connect the aircraft to its transition vehicle.next-transition.transition-aircraft := vehicle; end method initialize;
170
// The next step an aircraft might be able to make define class <aircraft-transition> (<object>) slot transition-aircraft :: <aircraft>, init-keyword: aircraft:; slot from-container :: <vehicle-storage>, init-keyword: from:; slot to-container :: <vehicle-storage>, init-keyword: to:; // The earliest possible time that the transition could take place slot earliest-arrival :: <time-of-day>, init-keyword: arrival:; // Has this transition already been entered in the sorted sequence? // This flag saves searching the sorted sequence slot pending? :: <boolean> = #f, init-keyword: pending?:; end class <aircraft-transition>; // Describes one step of an aircrafts movements define method say (transition :: <aircraft-transition>) => () say(transition.earliest-arrival); format-out(": "); say(transition.transition-aircraft); format-out(" at "); say(transition.to-container); end method say; // Commercial aircraft are aircraft that may have a flight // assigned to them define abstract class <commercial-aircraft> (<aircraft>) slot aircraft-flight :: false-or(<flight>) = #f, init-keyword: flight:; end class <commercial-aircraft>; define method say (object :: <commercial-aircraft>) => () let flight = object.aircraft-flight; if (flight) say(flight); else format-out("Unscheduled Aircraft %s", object.vehicle-id); end if; end method say; // The class that represents all commericial Boeing 707 aircraft define class <B707> (<commercial-aircraft>) inherited slot cruising-speed, init-value: 368; inherited slot standard-size, init-value: make(<size>, length: 153, width: 146, height: 42); end class <B707>; define method say (aircraft :: <B707>) => () if (aircraft.aircraft-flight) next-method(); else format-out("Unscheduled B707 %s", aircraft.vehicle-id); end if; end method say;
In the preceding code, we model everything from the most general class of vehicle down to the specic class that represents the Boeing 707. We also model the transition steps that an aircraft may take as it travels throughout the airport, and the airlines and ights associated with commercial aircraft.
171
Airports Finally, we present the class that represents the entire airport and provide the method that briey describes the airport. The airport-classes.dylan le. (continued)
// AIRPORTS // The class that represents all places where people and aircraft meet define class <airport> (<physical-object>) // The name of the airport, such as "San Fransisco International Airport" slot name :: <string>, init-keyword: name:; // The three letter abbreviation, such as "SFO" slot code :: <string>, init-keyword: code:; // The airspace above the airport slot sky-above :: <sky>; end class <airport>; define method say (airport :: <airport>) => () format-out("Airport %s", airport.code); end method say;
172
end method flying-time; // Computes the distance between an aircraft and an airport, // taking into account the altitude of the aircraft // Assumes the altitude of the aircraft is the height // above the ground level of the airport define method distance-3d (aircraft :: <aircraft>, destination :: <airport>) => (distance :: <single-float>) // Miles // Here, a squared plus b squared is equals to c squared, where c is the // hypotenuse, and a and b are the other sides of a right triangle sqrt((aircraft.altitude / $feet-per-mile) ^ 2 + distance-2d(aircraft.current-position, destination.current-position) ^ 2); end method distance-3d; // The distance between two positions, ignoring altitude define method distance-2d (position1 :: <relative-position>, position2 :: <absolute-position>) => (distance :: <single-float>) // Miles // When we have a relative position for the first argument (the // aircraft), we assume the relative position is relative to the second // argument (the airport) position1.distance; end method distance-2d; // // // // It would be sensible to provide a distance-2d method that computed the great-circle distance between two absolute positions Our example does not need this computation, which is beyond the scope of this book
// The time it takes to go from the point of touchdown to the entrance // to the taxiway define method brake-time (aircraft :: <b707>, runway :: <runway>) => (duration :: <time-offset>) make(<time-offset>, total-seconds: ceiling/(runway.physical-size.length / $feet-per-mile, $average-b707-brake-speed / $seconds-per-hour)); end method brake-time; // The time it takes to go from the entrance of the taxiway to the point // of takeoff define method takeoff-time (aircraft :: <b707>, runway :: <runway>) => (duration :: <time-offset>) make(<time-offset>, total-seconds: ceiling/(runway.physical-size.length / $feet-per-mile, $average-b707-takeoff-speed / $seconds-per-hour) + $takeoff-pause-time); end method takeoff-time; // The time it takes to taxi from the runway entrance across the taxiway // to the gate define method gate-time (aircraft :: <b707>, taxiway :: <taxiway>)
173
=> (duration :: <time-offset>) make(<time-offset>, total-seconds: ceiling/(taxiway.physical-size.length / $feet-per-mile, $average-b707-taxi-speed / $seconds-per-hour)); end method gate-time; // The time it takes to taxi from the gate across the taxiway to the // entrance of the runway define method runway-time (aircraft :: <b707>, taxiway :: <taxiway>) => (duration :: <time-offset>) gate-time(aircraft, taxiway); end method runway-time; // The time it takes to unload, service, and load an aircraft. define method gate-turnaround (aircraft :: <b707>, gate :: <gate>) => (duration :: <time-offset>) make(<time-offset>, total-seconds: $average-b707-gate-turnaround-time); end method gate-turnaround;
174
define method available? (vehicle :: <aircraft>, container :: <single-storage>, direction :: <symbol>) => (container-available? :: <boolean>) object-fits?(vehicle, container) & ~ (container.vehicle-currently-occupying); end method available?; // A multiple storage container is available if the aircraft fits into // the container, and there are not too many aircraft already queued in // the container for the specified direction define method available? (vehicle :: <aircraft>, container :: <multiple-storage>, direction :: <symbol>) => (container-available? :: <boolean>) object-fits?(vehicle, container) & size(container.vehicles-by-direction[direction]) < container.maxima-by-direction[direction]; end method available?; // Avoids jamming the runway with inbound traffic, which would prevent // outbound aircraft from taking off // The runway is clear to inbound traffic only if there is space in the // next container inbound from the runway define method available? (vehicle :: <aircraft>, container :: <runway>, direction :: <symbol>) => (container-available? :: <boolean>) next-method() & select (direction) #"outbound" => #t; #"inbound" => let (class) = next-landing-step(container, vehicle); if (class) find-available-connection(container, class, vehicle) ~== #f; end if; end select; end method available?; // A slot is used to keep track of which aircraft is in a single // storage container define method move-in-vehicle (vehicle :: <aircraft>, container :: <single-storage>, direction :: <symbol>) => () container.vehicle-currently-occupying := vehicle; values(); end method move-in-vehicle; // A deque is used to keep track of which aircraft are traveling in a // particular direction in a multiple storage container define method move-in-vehicle (vehicle :: <aircraft>, container :: <multiple-storage>, direction :: <symbol>) => () let vehicles = container.vehicles-by-direction[direction]; push-last(vehicles, vehicle); values();
175
end method move-in-vehicle; // When an aircraft reaches the gate, it begins its outbound journey define method move-in-vehicle (vehicle :: <aircraft>, container :: <gate>, direction :: <symbol>) => () next-method(); vehicle.direction := #"outbound"; values(); end method move-in-vehicle; define method move-out-vehicle (vehicle :: <aircraft>, container :: <single-storage>, direction :: <symbol>) => () container.vehicle-currently-occupying := #f; values(); end method move-out-vehicle; define method move-out-vehicle (vehicle :: <aircraft>, container :: <multiple-storage>, direction :: <symbol>) => () let vehicles = container.vehicles-by-direction[direction]; // Assumes that aircraft always exit container in order, and // that this aircraft is next pop(vehicles); values(); end method move-out-vehicle; // Determines what vehicle, if any, could move to the next container // If there is such a vehicle, then this method returns the vehicle, // the next container in the direction of travel, // and the time that it would take to make that transition define method next-out (container :: <vehicle-storage>, direction :: <symbol>) => (next-vehicle :: false-or(<vehicle>), next-storage :: false-or(<vehicle-storage>), time-to-execute :: false-or(<time-offset>)); let next-vehicle = next-out-internal(container, direction); if (next-vehicle) let (class, time) = next-landing-step(container, next-vehicle); if (class) let next-container = find-available-connection(container, class, next-vehicle); if (next-container) values(next-vehicle, next-container, time); end if; end if; end if; end method next-out; // This method is just a helper method for the next-out method // We need different methods based on the class of container define method next-out-internal (container :: <single-storage>, desired-direction :: <symbol>) => (vehicle :: false-or(<aircraft>))
176
let vehicle = container.vehicle-currently-occupying; if (vehicle & vehicle.direction == desired-direction) vehicle; end; end method next-out-internal; define method next-out-internal (container :: <multiple-storage>, desired-direction :: <symbol>) => (vehicle :: false-or(<aircraft>)) let vehicle-queue = container.vehicles-by-direction[desired-direction]; if (vehicle-queue.size > 0) vehicle-queue[0]; end; end method next-out-internal; // The following methods return the class of the next container to which a // vehicle can move from a particular container // They also return an estimate of how long that transition will take define method next-landing-step (storage :: <sky>, aircraft :: <aircraft>) => (next-class :: false-or(<class>), duration :: false-or(<time-offset>)) if (aircraft.direction == #"inbound") values(<runway>, flying-time(aircraft, storage.airport-below)); end if; end method next-landing-step; define method next-landing-step (storage :: <runway>, aircraft :: <aircraft>) => (next-class :: <class>, duration :: <time-offset>) select (aircraft.direction) #"inbound" => values(<taxiway>, brake-time(aircraft, storage)); #"outbound" => values(<sky>, takeoff-time(aircraft, storage)); end select; end method next-landing-step; define method next-landing-step (storage :: <taxiway>, aircraft :: <aircraft>) => (next-class :: <class>, duration :: <time-offset>) select (aircraft.direction) #"inbound" => values(<gate>, gate-time(aircraft, storage)); #"outbound" => values(<runway>, runway-time(aircraft, storage)); end select; end method next-landing-step; define method next-landing-step (storage :: <gate>, aircraft :: <aircraft>) => (next-class :: <class>, duration :: <time-offset>) values(<taxiway>, gate-turnaround(aircraft, storage)); end method next-landing-step;
The scheduling algorithm The next methods form the core of the airport application. The schedule.dylan le. (continued)
// Searches all of the vehicle storage of class class-of-next, which is // connected to container and has room for aircraft define method find-available-connection (storage :: <vehicle-storage>, class-of-next :: <class>, aircraft :: <aircraft>)
177
=> (next-container :: false-or(<vehicle-storage>)) block (return) for (c in storage.connected-to) if (instance?(c, class-of-next) & available?(aircraft, c, aircraft.direction)) return(c); end if; end for; end block; end method find-available-connection; // Generate new transitions to be considered for the next move // The transitions will be placed in the sorted sequence, which will order // them by earliest arrival time define method generate-new-transitions (container :: <vehicle-storage>, active-transitions :: <sorted-sequence>, containers-visited :: <object-table>) => () unless(element(containers-visited, container, default: #f)) // Keep track of which containers we have searched for new possible // transitions // We avoid looping forever by checking each container just once containers-visited[container] := #t; local method consider-transition (direction) // See whether any vehicle is ready to transition out of a container let (vehicle, next-container, time) = next-out(container, direction); unless (vehicle == #f | vehicle.next-transition.pending?) // If there is a vehicle ready, and it is not already in the // sorted sequence of pending transitions, then prepare the // transition instance associated with the vehicle let transition = vehicle.next-transition; transition.from-container := container; transition.to-container := next-container; // The vehicle may have been waiting // Take this situation into account when computing the earliest // arrival into the next container transition.earliest-arrival := transition.earliest-arrival + time; // Flag the vehicle as pending, to save searching through the // active-transitions sorted sequence later transition.pending? := #t; // Add the transition to the set to be considered add!(active-transitions, transition); end unless; end method consider-transition; // Consider both inbound and outbound traffic consider-transition(#"outbound"); consider-transition(#"inbound"); // Make sure that every container connected to this one is checked for (c in container.connected-to) generate-new-transitions(c, active-transitions, containers-visited); end for; end unless; end method generate-new-transitions;
178
// Main loop of the program // See what possible transitions exist, then execute the earliest // transitions that can be completed // Returns the time of the last transition define method process-aircraft (airport :: <airport>, #key time = $midnight) => (time :: <time-of-day>) format-out("Detailed aircraft schedule for "); say(airport); format-out("\n\n"); let sky = airport.sky-above; let containers-visited = make(<object-table>); let active-transitions = make(<sorted-sequence>, value-function: earliest-arrival); // We do not have to use return as the name of the exit procedure block (done) while (#t) // Each time through, start by considering every container fill!(containers-visited, #f); // For every container, see if any vehicles are ready to transition // If any are, add transition instances to the active-transitions // sorted sequence generate-new-transitions(sky, active-transitions, containers-visited); // If there are no more transitions, we have completed our task if (empty?(active-transitions)) done(); end; // Find the earliest transition that can complete, because there is // still room available in the destination container let transition-index = find-key(active-transitions, method (transition) available?(transition.transition-aircraft, transition.to-container, transition.transition-aircraft.direction); end); // // // if If none can complete, there is a problem with the simulation This situation should never occur, but is useful for debugging incorrect container configurations (transition-index == #f) error("Pending transitions but none can complete."); end if; // Otherwise, the earliest transition that can complete has been // found: Execute the transition let transition = active-transitions[transition-index]; let vehicle = transition.transition-aircraft; let vehicle-direction = vehicle.direction; move-out-vehicle(vehicle, transition.from-container, vehicle-direction); move-in-vehicle(vehicle, transition.to-container, vehicle-direction); // This transition is complete; remove it from consideration transition.pending? := #f; remove!(active-transitions, transition); // Compute the actual time of arrival at the next container, and
179
// display the message time := (transition.earliest-arrival := max(time, transition.earliest-arrival)); say(transition); format-out("\n"); end while; end block; time; end method process-aircraft;
The process-aircraft method uses components from the time, space and sorted sequence libraries, the container classes and protocols, and the vehicle classes and methods to schedule the aircraft arriving and departing from an airport. The generate-new-transitions method assists by examining the current state of all containers in the airport, and by noting any new steps that vehicles could take.
180
position-report-time :: <time-of-day> = make(<time-of-day>, total-seconds: encode-total-seconds(6, 0, 0))) => (airport :: <airport>) let gates = generate-gates(gates-per-terminal, capacity); let taxiway = make(<taxiway>, id: "Echo", directions: #[#"inbound", #"outbound"], maxima: vector(taxiway-count, taxiway-count), capacity: capacity, physical-size: taxiway-size); let runway = make(<runway>, id: "11R-29L", capacity: capacity, physical-size: runway-size); let keystone-air = make(<airline>, name: "Keystone Air", code: "KN"); let flights = map(method (fn) make(<flight>, airline: keystone-air, number: fn) end, *flight-numbers*); let aircraft = map(method (aircraft-flight, aircraft-distance, aircraft-heading, aircraft-altitude, aircraft-id) make(<b707>, flight: aircraft-flight, current-position: make(<relative-position>, distance: aircraft-distance, angle: make(<relative-angle>, total-seconds: encode-total-seconds (aircraft-heading, 0, 0))), altitude: aircraft-altitude, id: aircraft-id, transition: make(<aircraft-transition>, arrival: position-report-time)); end, flights, *aircraft-distances*, *aircraft-headings*, *aircraft-altitudes*, *aircraft-ids*); let airport = make(<airport>, name: "Belefonte Airport", code: "BLA", current-position: make(<absolute-position>, latitude: make(<latitude>, total-seconds: encode-total-seconds(40, 57, 43), direction: #"north"), longitude: make(<longitude>, total-seconds: encode-total-seconds(77, 40, 24), direction: #"west"))); let sky = make(<sky>, inbound-aircraft: aircraft, airport: airport, id: concatenate("over ", airport.code)); airport.sky-above := sky; runway.connected-to := vector(taxiway, sky); let taxiway-vector = vector(taxiway);
181
for (gate in gates) gate.connected-to := taxiway-vector; end for; let runway-vector = vector(runway); taxiway.connected-to := concatenate(runway-vector, gates); sky.connected-to := runway-vector; airport; end method build-simple-airport; define method test-airport () => (last-transition :: <time-of-day>) process-aircraft(build-simple-airport()); end method test-airport;
182
183
export <size>, length, height, width, current-position, current-position-setter; export physical-size, physical-size-setter, $default-capacity; export storage-capacity, storage-capacity-setter, identifier; export connected-to, connected-to-setter; export <gate>, generate-gates, <sky>, <runway>, <taxiway>; export <airline>, name, name-setter, code, code-setter, <flight>; export aircraft-flight, aircraft-flight-setter, number, number-setter, altitude, altitude-setter; export <aircraft-transition>, <b707>, <airport>, sky-above, sky-above-setter; export process-aircraft; use dylan; use transcendentals, import: {sqrt}; use say; use format-out, import: {format-out}; use format, import: {format-to-string}; use definitions; use sorted-sequence; use time; use angle, export: {direction, direction-setter}; use position; end module airport;
184
5.3.12 Summary
In this chapter, we presented a complete rst draft of the airport application, based on the techniques presented in previous chapters. Although the example is complete and meets its stated design goals, we can still make a number of improvements. For example, we could take advantage of Dylans multiple inheritance to eliminate certain repetitive slots. We could provide a container-implementor module interface, and open the classes and generic functions so that users could add their own classes of containers and extend the scope of the application. We could take advantage of Dylans exception handling to better deal with unusual situations that might occur during the simulation. In the chapters that follow, we show the Dylan language features that enable such improvements. Design of the Airport Application, describes the goals and overall design of the airport example. Denition of a New Collection, shows you how to build a new class of sequence, called a sorted sequence. The Airport Application, contains the complete, working code of the airport example. This chapter illustrates many techniques described in the previous chapters, including collections, control-ow operators, initialization of slots, and libraries and modules. The chapters in Part Part 4. Advanced Topics describe advanced techniques that you can use to improve the code presented in Part 3. Sample Application.
185
186
CHAPTER
SIX
187
Figure 6.1: Hierarchy of vehicle classes. <vehicle>. Lets look at our current denitions of both the <vehicle> class and its only direct subclass, <aircraft>:
// The class that represents all self-propelled devices define abstract class <vehicle> (<physical-object>) // Every vehicle has a unique identification code slot vehicle-id :: <string>, required-init-keyword: id:; // The normal operating speed of this class of vehicle in miles per hour each-subclass slot cruising-speed :: <positive-integer>; // Allow individual differences in the size of particular aircraft, while // providing a suitable default for each class of aircraft each-subclass slot standard-size :: <size>; end class <vehicle>; // This class represents vehicles that normally fly for a portion of // their trip define abstract class <aircraft> (<vehicle>) slot altitude :: <integer>, init-keyword: altitude:; // Direction here is either #inbound or #outbound. slot direction :: <symbol>; // The next transition that this aircraft might be able to make. slot next-transition :: <aircraft-transition>, required-init-keyword: transition:, setter: #f; end class <aircraft>;
As a start, we can dene a <fuel-truck> class as a subclass of <vehicle>. To operate on instances of this class, we will no doubt need to know how much aircraft fuel they contain. We dene one initial slot, aircraft-fuel-remaining. We also need to provide initial values for the inherited slots cruising-speed and standard-size.
define class <fuel-truck> (<vehicle>) // Amount of aircraft fuel remaining in the tank slot aircraft-fuel-remaining :: <integer>, init-keyword: aircraft-fuel-remaining:, init-value: 0; inherited slot cruising-speed, init-value: 25; inherited slot standard-size, init-value: make(<size>, length: 30, width: 10, height: 10); end class <fuel-truck>;
188
This denition serves our immediate purpose, but the class hierarchy is not as modular as it might be. Suppose that we want to take account of other vehicles on the ground, such as baggage carriers or re trucks? We can anticipate that all ground vehicles might have common features, and we do not want each new class to be a direct subclass of <vehicle> . As a renement, we dene two intermediary classes, <ground-vehicle> and <flying-vehicle>:
define abstract class <ground-vehicle> (<vehicle>) end class <ground-vehicle>; define abstract class <flying-vehicle> (<vehicle>) end class <flying-vehicle>; define class <fuel-truck> (<ground-vehicle>) // How much aircraft fuel is left in the tank slot aircraft-fuel-remaining :: <integer>, init-keyword: aircraft-fuel-remaining:, init-value: 0; inherited slot cruising-speed, init-value: 25; inherited slot standard-size, init-value: make(<size>, length: 30, width: 10, height: 10); end class <fuel-truck>; define abstract class <aircraft> (<flying-vehicle>) slot altitude :: <integer>, init-keyword: altitude:; slot direction :: <symbol>; slot next-transition :: <aircraft-transition>, required-init-keyword: transition:, setter: #f; end class <aircraft>;
At this point, we are going to leave the fuel-truck simulation. We do not model the fuel-supply problem further in this book. We do want to explore opportunities that our new class hierarchy presents for restructuring the aircraft classes. Aircraft classes and multiple inheritance It is obvious that an aircraft is a ying vehicle. In our airport model, however, we have to take account of an aircrafts behavior on taxiways and runways and at gates. In these situations, the aircraft is acting as a ground vehicle. Perhaps it makes sense to dene our aircraft classes as subclasses of both <flying-vehicle> and <ground-vehicle>. What could we gain by doing so? Consider cruising speed. When an aircraft is in the air, we need to take into account its ying cruising speed when estimating its time of arrival at its destination. When the aircraft is on the ground, we need to take into account the ground cruising speed when estimating how much time the aircraft will spend on a taxiway or runway. It makes sense to have both ying and ground cruising speeds. It also makes sense for ying cruising speed to be a property of ying vehicles more specically, aircraft and for ground cruising speed to be a property of ground vehicles. After all, the notion of cruising speed can be useful in estimating how long a fuel truck will take to arrive at a given gate. We now restructure our vehicle classes again, this time to make the aircraft classes be subclasses of both <flying-vehicle> and <ground-vehicle>. We need to remove the cruising-speed slot from the <vehicle> class, and to replace it by two slots: ground-cruising-speed for the <ground-vehicle> class and flying-cruising-speed for the <flying-vehicle> class. We can also take this opportunity to move the altitude slot from the <aircraft> class to the <flying-vehicle> class, because any ying vehicle is likely to need to keep track of its altitude. Finally, we introduce multiple inheritance by redening the <aircraft> class to be a direct subclass of both <flying-vehicle> and <ground-vehicle>.
define abstract class <vehicle> (<physical-object>) // Every vehicle has a unique identification code slot vehicle-id :: <string>, required-init-keyword: id:; // The standard size of this class of vehicle
189
each-subclass slot standard-size :: <size>; end class <vehicle>; define abstract class <ground-vehicle> (<vehicle>) // The normal operating speed of this class of vehicle each-subclass slot ground-cruising-speed :: <positive-integer>; end class <ground-vehicle>; define abstract class <flying-vehicle> (<vehicle>) // The normal operating speed of this class of vehicle each-subclass slot flying-cruising-speed :: <positive-integer>; slot altitude :: <integer>, init-keyword: altitude:; end class <flying-vehicle>; define abstract class <aircraft> (<flying-vehicle>, <ground-vehicle>) slot direction :: <symbol>; slot next-transition :: <aircraft-transition>, required-init-keyword: transition:, setter: #f; end class <aircraft>;
Now, all aircraft classes have two slots for cruising speed: ground-cruising-speed, inherited from the <ground-vehicle> class, and flying-cruising-speed, inherited from the <flying-vehicle> class. We have to modify our <B707> class to provide default initial values for these slots.
define class <B707> (<commercial-aircraft>) inherited slot flying-cruising-speed, init-value: 368; inherited slot ground-cruising-speed, init-value: 45; inherited slot standard-size, init-value: make(<size>, length: 153, width: 146, height: 42); end class <B707>;
Finally, to complete the example, we would change our <fuel-truck> class denition to provide a default initial value for ground-cruising-speed, instead of cruising-speed.
denition an inherited slot option that includes an init-value: or init-function: specication, or an init expression. Suppose that more than one class denes a default value for the same slot. Which default takes precedence? When each class has only one direct superclass, the answer is easy: the default value provided by the most specic class takes precedence. A default value for a subclass overrides a default value for a superclass. But what if a class has more than one direct superclass, and each superclass provides a different default value for the same slot? Imagine, for example, that our <vehicle> class had a slot named fuel-remaining, and our <ground-vehicle> and <flying-vehicle> classes each had a different default value for the fuel-remaining slot, which they inherit from the common superclass <vehicle>:
define abstract class <vehicle> (<physical-object>) slot fuel-remaining :: <integer>; ... end class <vehicle>; define abstract class <ground-vehicle> (<vehicle>) inherited-slot fuel-remaining, init-value: 30; ... end class <ground-vehicle>; define abstract class <flying-vehicle> (<vehicle>) inherited-slot fuel-remaining, init-value: 3000; ... end class <flying-vehicle>; define abstract class <aircraft> (<flying-vehicle>, <ground-vehicle>) ... end class <aircraft>;
Now neither the class <ground-vehicle> nor the class <flying-vehicle> is more specic than the other with respect to <aircraft>. So when we create an instance of <aircraft> that has both <ground-vehicle> and <flying-vehicle> as direct superclasses, what is the default initial value for the fuel-remaining slot: 30 or 3000? To answer this question, Dylan needs an additional way of ordering classes, called a class precedence list. In The class precedence list, we describe how Dylan constructs the class precedence list. The short answer to our question about default initial slot values is that Dylan uses the default value provided by the class that appears earlier in the class precedence list. We shall see that the class precedence list is also important for method dispatch in the presence of multiple inheritance. Suppose, for example, that we had dened two getter or two setter methods for the fuel-remaining slot: one specialized on the <flying-vehicle> class, and the other specialized on the <ground-vehicle> class. Which method would be selected to get or set the slot value of an instance of <aircraft> ? We return to the issue of method dispatch after we see how Dylan constructs the class precedence list.
specic than are any of its superclasses. But we cannot always order its superclasses in terms of specicity. Graph of vehicle classes that use multiple inheritance. illustrates our current denitions of <vehicle> and of <vehicle> s subclasses.
Figure 6.2: Graph of vehicle classes that use multiple inheritance. Consider <B707> and its superclasses. We can order <B707>, <commercial-aircraft>, and <aircraft> from more specic to less specic. But we cannot say that either <ground-vehicle> or <flying-vehicle> is more specic than the other, because neither class is a subclass of the other. We could order <B707> and its superclasses in two ways, from more specic to less specic:
<B707>, <commercial-aircraft>, <aircraft>, <flying-vehicle>, <ground-vehicle>, <vehicle>, <physical-object>, <object> <B707>, <commercial-aircraft>, <aircraft>, <ground-vehicle>, <flying-vehicle>, <vehicle>, <physical-object>, <object>
Dylan needs a way to determine which of these orderings to use. It solves the problem by constructing a class precedence list for <B707> and its superclasses.
192
Construction of the class precedence list To understand how Dylan determines the class precedence list, recall that the define class form for a class includes a list of superclasses. Remember that we dened <aircraft> as follows:
define abstract class <aircraft> (<flying-vehicle>, <ground-vehicle>) ... end class <aircraft>;
Here, we have listed the superclasses as <flying-vehicle> and <ground-vehicle>, in that order. In creating the class precedence list for a class, Dylan uses the ordering of the list of direct superclasses in the define class form for that class. Dylan relies on the following rules: 1. The class being dened takes precedence over all its direct superclasses. 2. Each direct superclass in the list takes precedence over all direct superclasses that appear later in the list. These rules establish an ordering of a class and its direct superclasses, called the local precedence order. We listed <flying-vehicle> before <ground-vehicle> in the list of superclasses of <aircraft>, so, when we apply these rules, we see that, for the <aircraft> class, <flying-vehicle> must have precedence higher than that of <ground-vehicle>. The local precedence order for <aircraft> is as follows: <aircraft>, <ying-vehicle>, <ground-vehicle> The local precedence order for a class establishes an ordering of a class and its direct superclasses. But our goal is to produce an overall class precedence list, which establishes an ordering of the class and all its superclasses, direct and indirect. In constructing the class precedence list for a class, Dylan follows two steps: 1. Construct the local precedence order for the class and its direct superclasses, based on the order in which the direct superclasses appear in the define class form for the class. 2. Construct the overall class precedence list for the class by merging the classs local precedence order with the class precedence lists of the classs direct superclasses. Notice that this procedure is recursive! But it is guaranteed to terminate, because no class can be its own superclass. The resulting class precedence list must be consistent with the local precedence order of the class, and with the class precedence list of each direct superclass. If class <a> precedes class <b> in the class precedence list, then <b> cannot precede <a> in either the local precedence order or the class precedence list for any direct superclass. Because of the recursive procedure for constructing it, the class precedence list must be consistent with the local precedence orders and class precedence lists of all the classs superclasses, rather than just with those of the direct superclasses. We can now see how Dylan computes the class precedence list for the <B707> class: 1. Construct the local precedence order for <B707> and its only direct superclass, <commercial-aircraft>. The result is as follows: <B707>, <commercial-aircraft>. 2. Merge the local precedence order with the class precedence list of the only direct superclass, <commercial-aircraft>. Dylan must now use these rules, recursively, to compute the class precedence list of <commercial-aircraft>. In doing so, Dylan must compute recursively the class precedence list of the only direct superclass of <commercial-aircraft>: <aircraft>. This process continues until Dylan has recursively computed the class precedence lists for all superclasses of <B707>. Finally, Dylan nishes constructing the class precedence list for <B707> itself. class-precedence-lists-for-b707 shows the results. One implication of this procedure is that, if a class inherits a superclass via two different paths, the superclass in common must have precedence lower than that of any of its subclasses. For example, the <object> class is a superclass of
193
Table 6.1: Class precedence lists for <B707> and its superclasses. Class <object> <physicalobject> <vehicle> Local precedence order <object> <physical-object>, <object> <vehicle>, <physical-object> <ground<ground-vehicle>, vehicle> <vehicle> <ying<ying-vehicle>, vehicle> <vehicle> <aircraft> <aircraft>, <ying-vehicle>, <ground-vehicle> <commercial- <commercial-aircraft>, aircraft> <aircraft> <B707> <B707>, <commercial-aircraft> Class precedence list <object> <physical-object>, <object> <vehicle>, <physical-object>, <object> <ground-vehicle>, <vehicle>, <physical-object>, <object> <ying-vehicle>, <vehicle>, <physical-object>, <object> <aircraft>, <ying-vehicle>, <ground-vehicle>, <vehicle>, <physical-object>, <object> <commercial-aircraft>, <aircraft>, <ying-vehicle>, <ground-vehicle>, <vehicle>, <physical-object>, <object> <B707>, <commercial-aircraft>, <aircraft>, <ying-vehicle>, <ground-vehicle>, <vehicle>, <physical-object>, <object>
every class (except itself). This class must have lower precedence than any of its subclasses, so it appears last in every class precedence list. The class precedence list is consistent with the rule that a subclass is more specic than are any of its superclasses. More complicated class precedence lists Sometimes, more than one class precedence list is consistent with the procedure that we have outlined so far. Suppose, for example, that we had dened two additional classes, <wheeled-vehicle> and <winged-vehicle>, with the class relations illustrated in Expanded graph of vehicle classes that use multiple inheritance.. Lets assume that the define class form for <aircraft> lists <winged-vehicle> before <wheeled-vehicle> in its list of direct superclasses. Now, three class precedence lists for <B707> are consistent with the procedures that we have discussed so far:
<B707>, <commercial-aircraft>, <aircraft>, <winged-vehicle>, <flying-vehicle>, <wheeled-vehicle>, <ground-vehicle>, <vehicle>, <physical-object>, <object> <B707>, <commercial-aircraft>, <aircraft>, <winged-vehicle>, <wheeled-vehicle>, <flying-vehicle>, <ground-vehicle>, <vehicle>, <physical-object>, <object> <B707>, <commercial-aircraft>, <aircraft>, <winged-vehicle>, <wheeled-vehicle>, <ground-vehicle>, <flying-vehicle>, <vehicle>, <physical-object>, <object>
In this case, Dylan uses an algorithm that tends to keep together, in the class precedence list, nonoverlapping superclass-to-subclass chains. Look at this situation another way: The algorithm Dylan uses to construct the class precedence list in effect builds the list one class at a time, from highest to lowest precedence. The class precedence list under construction for <B707> is unambiguous from <B707> through <winged-vehicle>. At that point, Dylan could insert either <flying-vehicle> or <wheeled-vehicle> into the list. It chooses the class that has a direct subclass rightmost in the partial class precedence list that it has already constructed. In this case, <flying-vehicle> has a direct subclass <winged-vehicle>, and <wheeled-vehicle> has a direct subclass <aircraft>. Because <winged-vehicle> is rightmost in the partial list already constructed, Dylan chooses <flying-vehicle> as 194 Chapter 6. Part 4. Advanced Topics
Figure 6.3: Expanded graph of vehicle classes that use multiple inheritance.
195
the next entry in the list. Once that decision has been made, the resulting class precedence list must be the rst of the three possible orderings that we listed:
<B707>, <commercial-aircraft>, <aircraft>, <winged-vehicle>, <flying-vehicle>, <wheeled-vehicle>, <ground-vehicle>, <vehicle>, <physical-object>, <object>
Note that it is not always possible to compute a class precedence list. Consider the three classes dened as follows:
define class <a> (<object>) ... end class <a>; define class <b> (<a>) ... end class <b>; define class <c> (<a>, <b>) ... end class <c>;
No class precedence list is possible for class <c> in this example, because the ordering of classes <a> and <b> conicts in the local precedence lists for classes <b> and <c>. Dylan signals an error when it tries to compute a class precedence list and nds that it cannot do so. To examine the class precedence list for a class, we use the all-superclasses function, which returns the class and its superclasses in the same order as they appear in the class precedence list:
? all-superclasses (<B707>) => #[{class <B707>}, {class <commercial-aircraft>}, {class <aircraft>}, => {class <winged-vehicle>}, {class <flying-vehicle>}, => {class <wheeled-vehicle>},{class <ground-vehicle>}, {class <vehicle>}, => {class <physical-object>}, {class <object>}]
The details of the algorithm that Dylan uses to construct class precedence lists are complicated, and are beyond the scope of this book. For most uncomplicated uses of simple inheritance, the most important points to remember about the class precedence list are that the list of direct superclasses in a define class form is ordered, and each direct superclass in the list takes precedence over all direct superclasses that appear later in the list. In general, if more than one superclass denes a behavior, the subclass behaves most like the rst superclass in its class precedence list that denes that behavior.
196
In the presence of multiple inheritance, it is possible to have two or more methods that are applicable, but that cannot be sorted by specicity because neither parameter type is a subtype of the other. By following only the rules that we have seen so far, Dylan cannot choose either method to call. Class precedence and method dispatch To see how this problem for method dispatch can arise, we return to our airport example. Recall that we now have two slots representing vehicle cruising speed: ground-cruising-speed for <ground-vehicle> and flying-cruising-speed for <flying-vehicle>. Lets dene a generic function, say-cruising-speed, to report the applicable cruising speed for each class:
define generic say-cruising-speed (vehicle :: <vehicle>); // Method 1 define method say-cruising-speed (vehicle :: <flying-vehicle>) format-out("Flying cruising speed: %d\n", vehicle.flying-cruising-speed); end method say-cruising-speed; // Method 2 define method say-cruising-speed (vehicle :: <ground-vehicle>) format-out("Ground cruising speed: %d\n", vehicle.ground-cruising-speed); end method say-cruising-speed; // Method 3 define method say-cruising-speed (vehicle :: <vehicle>) format-out("No cruising speed defined for type <vehicle>\n"); end method say-cruising-speed;
Now, suppose that we call say-cruising-speed on an instance of <B707>. Which method does Dylan call? All three methods are applicable. Both method 1 and method 2 are more specic than is method 3. But Dylan cannot order methods 1 and 2 by specicity. In this case, Dylan consults the class precedence list for the class of the argument. In our example, the class of the argument is <B707>. The <flying-vehicle> class takes precedence over the <ground-vehicle> class, because <flying-vehicle> precedes <ground-vehicle> in the list of direct superclasses for <aircraft>. Dylan calls method 1, which produces the following output:
Flying cruising speed: 368
Note that, if we had happened to list <ground-vehicle> before <flying-vehicle> in the list of direct superclasses for <aircraft>, Dylan would have called method 2, and we would have seen the following output:
Ground cruising speed: 45
In dening classes of aircraft, we did not intend for <flying-vehicle> characteristics to override <ground-vehicle> characteristics. But for method dispatch to work in the presence of multiple inheritance, Dylan must order subclasses and superclasses whenever it can. How can we change our example to make <flying-vehicle> behavior add to, rather than override, <ground-vehicle> behavior? By using next-method in our say-cruising-speed methods for <flying-vehicle> and <ground-vehicle>, we can report all applicable kinds of cruising speed for any combination of either or both of those classes*.* To make this behavior work, we also change the say-cruising-speed method for <vehicle>, which will always be called last, to have no effect:
// Method 1 define method say-cruising-speed (vehicle :: <flying-vehicle>)
197
format-out("Flying cruising speed: %d\n", vehicle.flying-cruising-speed); next-method(); end method say-cruising-speed; // Method 2 define method say-cruising-speed (vehicle :: <ground-vehicle>) format-out("Ground cruising speed: %d\n", vehicle.ground-cruising-speed); next-method(); end method say-cruising-speed; // Method 3 define method say-cruising-speed (vehicle :: <vehicle>) end method say-cruising-speed;
Recall that, when Dylan decides which method to call, the result is a list of methods, sorted by specicity. When say-cruising-speed is called on an instance of <B707>, the list of methods is sorted in the following order: method 1, method 2, method 3. Dylan calls method 1. Then, as a result of the call to next-method in method 1, Dylan calls method 2. Finally, as a result of the call to next-method in method 1, Dylan calls method 3. The output we see is as follows:
Flying cruising speed: 368 Ground cruising speed: 45
Note that, if we called say-cruising-speed on an instance of <fuel-truck>, we would see the following output:
Ground cruising speed: 25
Rened rules for method dispatch In summary, the effect of multiple inheritance on method dispatch is to rene the rule for sorting methods according to specicity: A method is more specic than another if the type of its specialized parameter is a proper subtype of the type of the other methods specialized parameter. (For denitions of proper subtype, see Method dispatch and nonclass types.) If one type is not a proper subtype of the other, a method is more specic if the class of its specialized parameter precedes the class of the other methods specialized parameter in the class precedence list of the argument to the generic function. Otherwise, the methods are unordered for that parameter. If the generic function has more than one required argument, Dylan uses this augmented rule for determining specicity in the usual way for sorting applicable methods with more than one argument. In essence, Dylan orders the applicable methods separately for each required argument, and then constructs an overall ordering by comparing the separate sorted lists. In the overall method ordering, a method is more specic than another if it satises two constraints: 1. The method is no less specic than the other method for all required parameters. (The two methods might have the same types for some parameters.) 2. The method is more specic than the other method for some required parameter. Note that one method might be more specic than another for one parameter, but less specic for another parameter. These two methods are ambiguous in specicity and cannot be ordered. If the method-dispatch procedure cannot nd any method that is more specic than all other methods, Dylan signals an error.
198
Comparison with C++: Multiple inheritance in C++ is different from multiple inheritance in Dylan. In C++, unless a base class is virtual, it is inherited multiple times if there is more than one path to the base class as a result of multiple inheritance. In Dylan, all base classes are effectively virtual. C++ has nothing like Dylans class precedence list for determining the precedence of two superclasses, neither of which is derived from the other. There is no implicit ordering of virtual members dened for such classes. C++ also has nothing like Dylans next-method for invoking the next most specic virtual function. A C++ programmer must often explicitly provide the sort of method dispatch and combination that Dylan implements automatically. For examples of similar Dylan and C++ programs that use multiple inheritance, see The concept of classes.
Comparison with Java: Java formalizes the concept of a protocol with its interfaces. An interface is like an abstract class and a set of required generic functions. A class that implements an interface must dene methods for each of the generic functions specied by that interface. In a sense, an interface is like a specication for multiple inheritance, without the implementation. A class that implements an interface is considered to be of the interface type, but it must implement all the behaviors directly, rather than inheriting them from the interface which may mean that code has to be duplicated, rather than shared and reused.
199
slot name :: <string>, init-keyword: name:; ... end class <airport>; define class <airline> (<object>) slot name :: <string>, required-init-keyword: name:; ... end class <airline>;
Our example would be more unied and maintainable if we had a single representation for these identiers. There are several ways that we could improve the example using single inheritance. One way to do that in principle would be to dene a name slot in a common superclass. In this case, we cannot use this solution, because the only common superclass is the built-in class <object>. This approach would work if all named classes inherited from <physical-object> we could add a name slot to <physical-object> . But then all subclasses of <physical-object> would inherit the name slot, whether or not those subclasses need names. Some objects might be inappropriately named, and those instances would be larger than they need to be. Another approach would be to dene two new subclasses to contain the name slot: a <named-object> subclass of <object>, and a <named-physical-object> subclass of <physical-object>. We would then use <named-physical-object> as the superclass for <vehicle-storage>, <vehicle>, and <airport>, and we would use <named-object> as the superclass for <airline>. That would work, too, although the name slot would be dened in two classes, rather than in one. Suppose, however, that we later nd that some, but not all, subclasses need another attribute, such as a unique identier. Perhaps <airport>, <vehicle>, and <airline> need unique identiers, but <vehicle-storage> does not. Extending this model, we might have to dene new classes <unique-object>, <unique-named-object>, <unique-physical-object>, and <unique-named-physical-object>. We now have eight base classes to represent the possible combinations of name and unique identier. If we add a third attribute, we end up with many more classes. We soon have an unmanageable proliferation of base classes. Multiple inheritance provides a solution to these problems. We can dene a mix-in class, name-mix-in, whose only purpose is to contain the name slot:
define abstract class <name-mix-in> (<object>) slot name :: <string>, init-keyword: name:; end class <name-mix-in>;
Now, we redene our <vehicle-storage>, <vehicle>, <airport>, and <airline> classes to have two direct superclasses: <name-mix-in>, and either <object> or <physical-object>:
define abstract class <vehicle-storage> (<name-mix-in>, <physical-object>) // identifier slot removed required keyword name:; ... end class <vehicle-storage>; define abstract class <vehicle> (<name-mix-in>, <physical-object>) // vehicle-id slot removed required keyword name:; ... end class <vehicle>; define class <airport> (<name-mix-in>, <physical-object>) // name slot removed keyword name:, init-value: "Anonymous Airport"; ...
200
end class <airport>; define class <airline> (<name-mix-in>, <object>) // name slot removed required keyword name:; ... end class <airline>;
We use the required keyword option to make the name: keyword required when we create an instance of <vehicle-storage>, <vehicle>, or <airline>. If we provided an init-value: or init-function: for the name slot in the denition of <name-mix-in>, Dylan would ignore that option when we created an instance of any of these subclasses. We also use the keyword option with an init-value: to provide a default initial value for the name: initialization argument and for the name slot for instances of <airport>. Of course, we also have to change other code in our example to use the name name and the init keyword name: when referring to the slot. Multiple inheritance provides several advantages in solving the name problem: 1. We localize in a single class the characteristic of having a name. 2. Subclasses can still customize aspects of the name attribute, such as what that attributes initial value is, and whether or not it is required. 3. We can give a subclass a name attribute without redening any of its superclasses. 4. The only subclasses that have a name attribute are those for which that is appropriate. Pros and cons of multiple inheritance There is debate about the value of using multiple inheritance in object-oriented programs. Some people think that multiple inheritance in appropriate applications can improve modularity and can make it easier to reuse code. Other people think that the complications and pitfalls of multiple inheritance make program maintenance difcult, and thus outweigh the possible advantages. We have presented examples of multiple inheritance that show that it can have advantages when you can separate object characteristics into non-overlapping sets. Multiple inheritance then lets you create complex classes using only the characteristics that you need, without a proliferation of base classes. Multiple inheritance does complicate method dispatch and impose additional requirements on an application. It is essential to be aware of dependencies on subclasssuperclass ordering, particularly in method selection and slot initialization. In general, classes that are intended to be multiple direct superclasses of the same subclass should depend on one another as little as possible. Protocols involving multiple inheritance may need more documentation than do those involving single inheritance.
6.1.6 Summary
In this chapter, we covered the following: We introduced the concept of multiple inheritance: inheritance from more than one direct superclass. We discussed the implications of multiple inheritance for slot initialization. We described how Dylan constructs the class precedence list for a class. The class precedence list is an ordering of a class and all its superclasses.
201
We showed how Dylan uses class precedence lists in sorting methods by specicity when a generic function is called. We developed extensions of the airport example using multiple inheritance. We discussed advantages and disadvantages of using multiple inheritance.
202
features, it can be hard for the programmer to develop an efciency model a model of the absolute or relative cost of different approaches to a problem. In contrast, in other languages, such as C, every language construct can be explained directly in terms of a small number of machine instructions. Although it may be easy to understand the performance of a C program in terms of a simple model, programming in C is more work for the programmer the higher-level abstractions are not provided, and must often be built from scratch. For example, a C programmer expects that the run-time cost of calling a function is the cost of possibly saving registers on a stack, passing the arguments, executing a machine instruction for jumping to a subroutine, and then executing a return instruction at the end of the function; if it is a call through a function pointer, or a C++ virtual function, the cost of an indirect jump must be added. In Dylan, the story is more complicated, because Dylan has a more sophisticated execution model: A call to a generic function might be much more expensive in a dynamic situation, because computing the most specic method could take much longer than would execution of the method itself. To write efcient programs in Dylan, you have to understand what constructs in the language can be expensive in time or space, and how you can reduce those costs in common cases. This understanding is based on an efciency model a conceptual model of how a program in Dylan runs at a low level. One problem with developing an efciency model is that there is no single way to implement many Dylan operations. Different compilers do things in different ways, and certain compilers have multiple techniques for compiling the same piece of code, depending on circumstances. Nonetheless, we shall try to give an intuitive feel for which features of Dylan are costly, and which features enable the compiler to make optimizations.
203
Comparison with C: Static languages such as C have little need for type inferencing, because the type of every value must be declared, and the types can be checked easily at compile time. On the other hand, when a problem domain is ill-specied, the program is evolving through development, or a value may take on one of several types, the programmer must construct union types, and must use variant records or other bookkeeping to track the actual type of the value manually. Dylan automatically handles this bookkeeping and uses type inferencing to minimize the associated overhead. At the same time, when the type of a variable can change at run time, Dylan also automatically tracks the changing type. Some compilers have a facility for generating performance warnings, which inform you when type inferencing is not able to determine types sufciently to generate optimal code. Some compilers have a facility for generating safety warnings, informing you when type inferencing is not able to determine types sufciently to omit run-time type checking. As an example, consider these denitions (which are similar to, but not exactly the same as, the denitions on which we settled in Four Complete Libraries):
define abstract open class <sixty-unit> (<object>) slot total-seconds :: <integer> = 0, init-keyword: total-seconds:; end class <sixty-unit>; define method decode-total-seconds (sixty-unit :: <sixty-unit>) => (hours :: <integer>, minutes :: <integer>, seconds :: <integer>) let total-seconds = abs(sixty-unit.total-seconds); let (total-minutes, seconds) = truncate/(total-seconds, 60); let (max-unit, minutes) = truncate/(total-minutes, 60); values (max-unit, minutes, seconds); end method decode-total-seconds;
Because we made the choice to store total-seconds as an integer, and because 60 is an integer constant, the compiler can infer that the truncate/ calls are for an integer divided by integer. There is no need to consider whether to use oating-point or integer division. If we were more concerned with testing out ideas, we might have left unspecied the type of the total-seconds slot (implicitly, its type would then be <object>), or, if we wanted to keep the option of having times more accurate than just seconds, we might have specied that its type was <real>, allowing for the possibility of using oatingpoint numbers, which can express fractional seconds. If we left the type of the total-seconds slot unspecied, the compiler would need to check the arguments to truncate/, on the off chance that an argument was not numeric at all. In some compilers, you would be able to get a compile-time safety warning stating that a run-time type error is possible (which, if unhandled, will result in program failure), and that the check, and the possibility of a run-time error, could be avoided if the compiler knew that total-seconds was a <real>. What is a safe program? Dylan is always safe in that a programming error cannot cause a corruption of the program (or of other programs). For example, an out-of-bound array access or passing an argument of incompatible type simply cannot happen. The compiler will either prove that the requested action is impossible, or will insert code to verify bounds or type at run time, and will signal an error if the bounds or type is incorrect. When we discuss safety in this section, we are referring to whether or not such errors will be visible to the user. If we have not provided for a recovery action, signaling of an error will halt the program. See Exceptions, for an example of how run-time errors can be handled by the program.
204
Comparison with Java: Java recognizes the need for safe operations, and has eliminated many of the unsafe practices of C and C++, adding such checks as array-bounds checks and type-cast checks at run time. However, Java retains the C mathematical model that trades performance for correctness. Java integers are of a xed size, and computations that cannot be represented in that size silently overow. In contrast, Dylan requires numeric operations to complete correctly or to signal an error. Several Dylan implementations are also expected to provide libraries for innite-precision numerical operations. If we specied the type of the total-seconds slot as <real>, the compiler would have to dispatch on the type of total-seconds, using either oating-point or integer division as necessary. In some compilers, we would be able to get a compile-time performance warning stating that this dispatch could be omitted if the compiler knew that total-seconds was of a more restricted type. Note that the type of the return value of decode-total-seconds can be inferred: max-unit and minutes must be <integer> (inferred from the denition of truncate/), and seconds must have the same type as total-seconds (<integer>, in our example); thus, the compiler does not have to insert any type checks on the return values of decode-total-seconds. Dylan enforces declared return types in the same way as it enforces parameter types, by eliminating the check where type inferencing can show it is not needed, and using the enforced types to make further inferences. From this example, you can see how the compiler can get a lot of mileage from a small number of constraints, and how it can point you to the places where further clarication will produce the most performance and safety benets. At the same time, Dylan does not require that you have all your types thought out in advance of compiling the program; the dynamic nature of the language allows Dylan to defer considering type information until the program is actually running. In good Dylan development environments, there is support for resolving and continuing from run-time type errors during program development (rather than requiring editing of the code and recompilation). Remember that your code is more suited to reuse when it has fewer and more general type constraints. If you have a compiler that can issue safety and performance notes, try to generalize and minimize your type constraints, being guided by your safety and performance requirements. Often, just the constraints required to specify method applicability will be sufcient for good safety and performance. Declaring the types of module variables, slots, and return values of functions is also useful and can help to document your program. Declaring types for constants and local variables can be useful for enforcing program correctness, but is unlikely to create optimization opportunities, and might actually reduce performance, because the compiler will insert type checks to enforce such constraints if they are overly restrictive.
205
Because the compiler can infer only that gates is a <vector>, it must generate extra code to determine whether each gate has a connected-to method on it. We can use limited types to constrain gate-instances as follows:
define constant <gate-vector> = limited(<vector>, of: <gate>); define method generate-gates (gates-per-terminal :: <vector>, default-gate-capacity :: <size>) => (gates :: <gate-vector>) let result = make(<gate-vector>, size: reduce1(\+, gates-per-terminal)); ... values(result); end method generate-gates;
With the limited constraint of the return value of generate-gates, the compiler can ensure that only gate objects will ever be stored in the vector; hence, it can be sure that each gate will be a <gate> and will have a connected-to method. Note that limited-collection types are instantiable types; that is, you can make an object of a limited type. This capability is different from similar constructs in certain other languages, in which those constructs are only an assertion about the range or type of values to be stored in the collection. Having declared the return value of generate-gates to be a <gate-vector>, it would be an error to return a <vector> instead; hence, we changed the argument to make when constructing result to be <gate-vector> instead of the original <vector>. If <gate> and connected-to are not open (as described in Open generic functions and Open classes), the compiler can infer that connected-to is used here to set a slot in the gate instance and to further optimize the code generated. We do not delve into the exact details of what the compiler has to know to make this optimization, but it is worth noting that, if either the class or the generic function were open, the optimization could not be made. Comparison with C++: The Dylan limited-collection types provide a capability similar to that offered by the C++ template classes. Unlike in C++, the base type of a limited-collection type (the equivalent of a C++ class template in the example above, <vector>) is also a valid type. Dylans dynamic capabilities mean that Dylan can defer determining the element type of a collection until run time, in effect adapting the class template as it goes along. By using a limited type, the compiler can generate more efcient code. Another use of limited types is to allow compact representations. We can use limited with the built-in type <integer> to specify numbers with a limited range that can be stored more compactly than integers. It is especially useful to use a limited range in combination with a limited collection; for example,
define constant <signed-byte-vector> = limited(<simple-vector>, of: limited(<integer>, min: -128, max 127));
In the preceding example, we dene a type that can be represented as a one-dimensional array of 8-bit bytes.
206
Comparison with C: C provides efcient data representations, because its data types typically map directly to underlying hardware representations. A drawback of C is that its efcient data representations are often not portable: The size of a short int may vary across platforms, for instance. Dylan takes the more abstract approach of describing the requirements of a data type, and letting the compiler choose the most efcient underlying representation. A drawback of the Dylan approach is that it cannot easily be used for low-level systems programming, where data structures must map reliably to the underlying hardware. Most Dylan systems provide a foreign-function interface to allow calling out to C or some other language more suitable to these low-level tasks. Some Dylan systems augment the language with machine-level constructs that provide the level of control necessary while staying within the object model as much as possible.
Comparison with Java: Java recognizes that portable programs need well-dened data types, rather than types that map to the particular underlying hardware differently in each implementation. However, Java retains some of Cs concreteness in simply specifying four distinct sizes of integer (in terms of how many binary digits they hold), and forcing the programmer to convert integer types to objects manually, when object-oriented operations are to be performed. In contrast, Dylans limited-integer types specify, at the program level, the abstract requirements of the type, giving the compiler freedom to map the program requirements as efciently as possible to the underlying architecture.
6.2.5 Enumerations
Many languages provide enumeration types both to enforce program correctness and to provide more compact representation of multiple-choice values. Dylan does not have a built-in enumeration type, but you can easily construct enumerations using the type-union and singleton type constructors. For example, consider the <latitude> and <longitude> classes, where there are only two valid values for the direction slot in each class. Rather than enforcing the restrictions programmatically, as we did in Virtual slots, we can create types that do the job for us:
define abstract class <directed-angle> (<sixty-unit>) slot direction :: <symbol>, required-init-keyword: direction:; end class <directed-angle>; define constant <latitude-direction> = type-union(singleton(#"north"), singleton(#"south")); define class <latitude> (<directed-angle>) keyword direction:, type: <latitude-direction>; end class <latitude>; define constant <longitude-direction> = type-union(singleton(#"east"), singleton(#"west")); define class <longitude> (<directed-angle>) keyword direction:, type: <longitude-direction>; end class <longitude>;
Here, the abstract superclass species that the read-only slot direction must be a <symbol>, and that it must be initialized when an instance is created with the keyword direction:. The constant <latitude-direction> is a type specication that permits only the symbol #"north" or the symbol #"south". The class <latitude> species that, when an instance of <latitude> is made, the initial value must be of the <latitude-direction> type. We handled the longitude case similarly. 6.2. Performance and Flexibility 207
The use of type-union and singleton to create enumeration types in this fashion is common enough that the function one-of is usually available in a utility library as a shorthand:
define constant one-of = method (#rest objects) apply(type-union, map(singleton, objects)) end method;
With this abbreviation, the direction types can be written more compactly:
define constant <latitude-direction> = one-of(#"north", #"south"); define constant <longitude-direction> = one-of(#"east", #"west");
Some Dylan compilers will recognize the idiomatic use of type-union and singleton to represent such enumerations more compactly. For instance, a compiler could represent the direction slot of a latitude or longitude as a single bit, using the getter and setter functions to translate back and forth to the appropriate symbol.
The inner call to decode-total-seconds can be a direct jump rather than a function call, because the compiler can infer which method should be called and that the return values already have the correct constraints.
208
When we dene a method without also dening a generic function, the compiler will generate an implicit generic function for us, which, in this case, will be as though we had dened the generic function like this:
define generic next-landing-step (o1 :: <object>, o2 :: <object>) => (#rest r :: <object>);
In The schedule.dylan le, where we did dene a generic function, we used a simple denition, just documenting the number of arguments, and giving them mnemonic names:
define generic next-landing-step (container, vehicle);
Because we did not specify types of the arguments or return values, they default to <object>, just as they did in the preceding implicit generic function. Although the generic function that we wrote does prevent us from dening methods with the wrong number of arguments, it does not constrain the types of those arguments or the format or type of return values in any way. A sophisticated compiler may be able to make inferences based on the methods that we dene, but we could both aid the compiler and more clearly document the protocol of next-landing-step by specifying the types of the parameters and return values in the denition of the generic function:
define generic next-landing-step (storage :: <vehicle-storage>, aircraft :: <aircraft>) => (next-storage :: <vehicle-storage>, elapsed-time :: <time-offset>);
Now, the compiler can help us. If we dene a method whose arguments are not a subclass of <vehicle-storage> and a subclass of <aircraft> (for example, if we provided the arguments in the wrong order), the compiler will report the error. Furthermore, the compiler can use the value declaration to detect errors in the return values (for example, if we returned only a single value or returned a value of the wrong type). Finally, the compiler can be asked to issue a warning if there is a subclass of the argument types for which no method is applicable. In addition to establishing a contract, specifying the types of the parameters and return values of generic functions can allow the compiler to make additional inferences, as described in Type constraints with regard to truncate/. In the absence of other information, the compiler is limited in the optimizations that it can make based solely on the parameter types in the generic function, so it is generally best not to restrict articially the types of a generic function, but rather to use the restricted types to document the generic functions protocol.
209
210
This statement is essentially a guarantee to the compiler that the only methods on say that are applicable to <time> objects (and also to <time-of-day> and <time-offset> objects, because <time-of-day> and <time-offset> are subclasses of <time>) are those that are dened explicitly in the time library (and in any libraries from which that one imports). Thus, when the compiler can prove that the argument to say is a <time-offset>, it can call the correct method directly, without any run-time dispatch overhead. Another way to get the same effect as a sealed domain, which is also self-documenting, is to use define sealed method when dening individual methods on the protocol. So, for instance, in the case of the time library, we might have dened the two methods on say as follows:
define sealed method say (time :: <time>) let (hours, minutes) = decode-total-seconds (time); format-out("%d:%s%d", hours, if (minutes < 10) "0" else " " end, minutes); end method say; define sealed method say (time :: <time-offset>) => () format-out("%s ", if (time.past?) "minus" else "plus" end); next-method(); end method say;
Dening a sealed method is the same as dening the generic function to be sealed over the domain of the methods specializers. In effect, this technique says that you do not intend anyone to add more specic methods in that domain, or to create classes that would change the applicability of the sealed methods. With either the define sealed domain form or the sealed methods, the use of say on <time> objects will be as efcient as it would be were say not an open generic function after all. At the same time, other libraries that create new classes can still extend the say protocol to cover those classes. Sealed domains impose restrictions on the ability of other libraries to create new methods, to remove new methods, and to create new classes: You cannot add methods to an open generic function imported from another library that would fall into the sealed domain of any other library. You can avoid this restriction by ensuring that at least one of the specializers of your method is a subtype of a type dened in your library.
211
Comparison with C++: A C++ compiler could optimize out the dispatching of a virtual function by analyzing the entire scope of the argument on which the virtual function dispatches, and proving that arguments exact class. Unfortunately, that scope is often the entire program, so this optimization often can be performed only by a linker. Even a linker cannot make this optimization when a library is compiled, because the classes of a library can be subclassed by a client. The complexity is compounded for dynamic-link libraries, where there may be multiple clients at once. As a result, this optimization is rarely achieved in C++. In Dylan, sealed classes, sealed generic functions, and sealed domains explicitly state which generic functions and classes may be extended, and, more important, which cannot. The library designer plans in advance exactly what extensibility the library will have. The Dylan compiler can then optimize dispatching on sealed generic functions and classes and within sealed domains with the assurance that no client will violate the assumptions of the optimization. The sealing restrictions against subclassing or changing method applicability are automatically enforced on each client of a Dylan library. When you seal a domain of a generic function imported from another library, you will not cause conicts with other libraries, as long as both of the following conditions hold: 1. At least one of the types in the sealed domain is a subtype of a class dened in your library 2. No additional subtypes can be dened for any of the types in the sealed domain In the case of a type that is a class, the rst condition means that you must have dened either the class or one of its superclasses in your library. The second condition means that the classes in the domain must not have any open subclasses (a degenerate case of which is a leaf class a class with no subclasses at all). If you need to seal a domain over a class that has open subclasses, you will need a thorough understanding of the sealing constraints detailed in The Dylan Reference Manual, but these two simple rules should handle many common cases. In our example, we obeyed both rules of thumb: our methods for say are on classes we dened, and our sealing was over classes that will not be further subclassed. The rules of thumb not only keep you from violating sealing constraints, they make for good protocol design: a library that extends a protocol really should extend it only for classes it fully understands, which usually means classes it creates. As an example of the restriction on subclassing open classes involved in a sealed domain, if the <time> class were an open class, we still could not add the following class in a library that used the time library:
define class <place-and-time> (<position>, <time>) end class <place-and-time>;
As far as the compiler is concerned, it knows that the only say method applicable to a <time> is the one in the time library. (That is what we have told it with our sealed domain denition.) It would be valid to pass a <place-and-time> object as an argument to a function that accepted <time> objects, but within that function the compiler might have already optimized a call to say to the method for <time> objects (based on <time> being in the sealed domain of say). But there is also a method for say on <position>, and, more important, we probably will want to dene a method specically for <place-and-time>. Because of this ambiguity, the class <place-and-time> cannot be dened in a separate library, and the compiler will signal an error. Note that the class <place-and-time> could be dened in the time library. The compiler can deal correctly with classes that may straddle a sealed domain, if they are known in the library where the sealed domain is dened. It would also be valid to subclass <time> in any way that did not change the applicability of methods in any sealed generic-function domains that include <time>. The actual rule involved depends on an analysis of the exact methods of the generic function, and the rule is complicated enough that you should just rely on your compiler to detect illegal situations.
212
That is, rather than giving the slot an initial value of 0 and an optional init-keyword:, we simply require that the slot be initialized when we make a <sixty-unit> object. Of course, the initial value must obey the type constraint of <integer>. The compiler can still make the inference that the slot will always be initialized and will always have an integer value. Comparison with C: Dylan always ensures that a slot is initialized before that slot is accessed, automatically inserting a run-time check when it cannot prove at compile time that the slot is always properly initialized. C puts this burden of safety on the programmer, and that can be the source of subtle bugs. A number of debugging and analysis tools are available as addons to C, to help the programmer with this task. Always initializing slots, either with a default value or required init-keyword, will make slot access efcient. Finally, in many cases, slots hold values that will not change over the lifetime of each instance (although they may be different values for each instance). In the case of the <sixty-unit> class, we never change the value of total-seconds. When adding two instances, we create a new one to hold the new value, rather than changing one of the argument instances (that way, we do not have to worry about changing an instance that may still be in use by some other part of the program). In such cases, declaring the slot to be constant both documents and enforces this intent. Furthermore, the compiler can often make additional optimizations for slots that are known never to be modied. The nal denition of <sixty-unit> is as follows:
213
define open abstract class <sixty-unit> (<object>) constant slot total-seconds :: <integer>, required-init-keyword: total-seconds:, end class <sixty-unit>;
(The constant declaration is simply shorthand for the slot option setter: set the slot.)
Although the generic function for the slot accessor total-seconds is sealed, and it is trivial for the compiler to infer that its argument is a <sixty-unit> in the call sixty-unit.total-seconds, because <sixty-unit> is declared open, the compiler cannot emit the most efcient code for that call. Because an open class could be mixed with any number of other classes, there is no guarantee that the slots of every object that is a <sixty-unit> will always be stored in the same order there is no guarantee that total-seconds will always be the rst slot in an object that is an indirect instance of <sixty-unit>, for instance. Declaring a class primary is essentially making a guarantee that the compiler can always put the primary classs slots in the same place in an instance, and that any other superclasses will have to adjust:
define abstract open primary class <sixty-unit> (<object>) constant slot total-seconds :: <integer>, required-init-keyword: total-seconds:; end class <sixty-unit>;
By adding the primary declaration to the denition, any library that subclasses <sixty-unit> is guaranteed to put total-seconds at the same offset. Hence, the compiler can turn the call sixty-unit.total-seconds into a single machine instruction (load with constant offset), without concern over which subclass of <sixty-unit> was passed as an argument. Comparison with C++: A primary class is like an ordinary base class in C++. Because only one primary class is allowed as a base class, its data members can be assigned the same xed offset for all derived classes. See The concept of classes, for a more detailed analogy. It is permissible to make subclasses of a primary class also primary, essentially freezing the assignment of all the slots in the subclass too. What is not permissible is to multiply inherit from more than one primary class; as you can see, such behavior would lead to a conict between the xed slot assignments.
214
Because primary classes restrict extension in this way, you should use them sparingly in libraries intended to be software components. Primary classes are of most benet in large, modular programs, where all the clients of each component are known, and the need for extensibility is bounded; typically that occurs toward the end of a project, when you are tuning for performance.
215
concerns for most programs. But some programs with specialized or tuned use of memory may run slower with automatic management. Whether storage management is automatic or manual, the use of memory raises performance issues. Every allocation of memory takes time, including the time to reclaim unused memory; either the programmer must free it explicitly, or the garbage collector has to do more work. It is obvious that calling a function such as make, vector, or pair in Dylan allocates memory, but there are operations that implicitly use memory. For example, creating a closure (see Closures) will usually cause Dylan to allocate memory for the closure. On the other hand, sometimes the compiler is able to prove that an object is never used after the function that creates it returns. In a good compiler, such objects are allocated on the stack, and are reclaimed automatically when the function exits. A good Dylan development environment will have tools that help you to meter and prole memory usage, so that you can adjust your program to utilize memory efciently. Inlining, constant folding, and partial evaluation One optimization that is common in many computer languages is inlining. Inlining replaces a call to a known function with the body of the function. Inlining is an important optimization in Dylan, because almost all Dylan operations slot access, array indexing, and collection iteration involve function calls. All good Dylan compilers, when compiling for speed, can be aggressive about inlining any computations, as long as doing so would not make a program grow too large. Constant folding (evaluating expressions involving constant values at compile time) and inlining are just two of the partial-evaluation techniques that you should expect to nd in any good Dylan compiler. Comparison with C: A programmer familiar with the optimizations done in C compilers can think of partial evaluation as an extreme combination of inlining and constant folding. One way in which Dylan has an advantage over C for partial evaluation is that it hard for a compiler to evaluate expressions that involve dereferencing pointers. For example, in C, it is difcult to evaluate partially a call to malloc, but Dylan compilers can often evaluate a call to make at compile time.
Type inference The quality of type inference can vary greatly among Dylan compilers. Type inference like most forms of program analysis works best with simple, straightforward code. Some constructs that are typically difcult for type inference are assignment and calling of block exit functions outside of the method that denes the block exit functions. One other way in which type constraints can be helpful is that they permit the compiler to choose efcient representations for objects. Most Dylan objects contain enough information for Dylan to determine their class this one is an important feature for the dynamic aspects of the language. But, suppose we have a 1000 x 1000 limited(<array>, of: <single-float>). There is no reason that each of the numbers in that array should also contain a reference to the <single-float> class; the one reference in the limited type is sufcient. (Note that, if we had used of: <real> or of: <float>, we would have needed more information, since multiple classes would have been possible.) When an object is represented in such a way, often many of the operations on it can be optimized. For example, the conventional representation of <double-float> will usually require an indirect-memory-reference machine instruction to get at the actual number, so adding two such objects is one oating-point machine instruction and two load-from-memory machine instructions; if a direct representation is used, just the add machine instruction is needed.
216
Further, if the return value is saved in a variable for which type information is not available, it may be necessary to allocate memory dynamically to store the return value. Types that may have more efcient representations include certain integer classes, the oating-point classes, characters, and Booleans. Precise declarations about these types, especially in slots and limited collections, can lead to signicant improvements in both the time and memory needed to run a program.
6.2.15 Summary
The most important point about performance is that it is important to pay attention to efciency during the entire design and development cycle of a project. During the design phase, try to ensure that the algorithms chosen have the right asymptotic behavior and constant factors, and that it is possible to implement the needed operations efciently. During the implementation phase, use the language constructs that most clearly express what the program is doing. Once the program is working correctly, it is then time to add type and sealing declarations, and to use metering and proling tools to nd and rewrite heavily used, slow parts of the program, in order to improve the performance. One of the most important considerations when programming is not to worry about performance too soon. It is always more important that your design and implementation be clear and correct, rst. There is no value in arriving at an answer with lightning speed, if it turns out to be the wrong answer. In this chapter, we covered the following: We showed how Dylan can balance performance and exibility to support a range of programming requirements. We showed how type constraints affect performance. We showed how limited types can improve performance. We showed how open generic functions provide modularity and exibility. We showed how open classes provide modularity and exibility. We showed how sealed generic function domains mitigate the performance penalty of open classes and generic functions. We showed how primary classes permit efcient slot access. We presented both an execution and efciency model that provides a conceptual model of how a program in Dylan runs, and what the relative cost of different program elements are. We examined the method constructs for exibility and performance available in Dylan; see Methods: exibility versus performance.
217
Table 6.2: Methods: exibility versus performance Construct direct method Effects highly optimizable no method dispatch highly optimizable not extensible by other libraries optimizable other libraries can subclass highly optimizable other libraries can add methods other libraries can subclass not optimizable methods can be added at run time subclasses can be created at run time
We discussed the constructs that can have type constraints, and the inuence on performance or exibility of using such a declaration; see Type constraint: exibility versus performance.. Table 6.3: Type constraint: exibility versus performance. Construct module constants module variables required parameters Effects enforce program correctness permit type inferencing required for method dispatch permit type inferencing permit type inferencing enforce program correctness permit type inferencing permit type inferencing permit compact data representation permit type inferencing
limited types
slots
218
6.3 Exceptions
An exception is an unexpected event that occurs during program execution (as opposed to problems detected during program compilation). One common type of exception is a violation of the contract of a function, such as attempting to divide a number by zero. Another example is an attempt to access an uninitialized slot, or certain cases of an attempt to violate the type constraint on a slot or variable (those that cannot be detected at compile time). Dylan detects all these exceptions itself. Sometimes, an application detects a violation of a contract that it denes. For example, in Virtual slots, we dened methods that detected attempts to specify a longitude direction of anything other than east or west. (In Enumerations we changed the application such that this particular application-detected exception was transformed into one that is detected by Dylan.) When an unusual event occurs in an application, there are many options available for responding to that event. The application can try to handle the situation in its own particular way, or it can use the exception protocol dened by Dylan. In this chapter, we explore several approaches to providing an exception protocol between parts of an application.
We have altered the + method in two important ways. First, we have modied the original values declaration, (sum :: <time-of-day>), to allow the return of either a <time-of-day> instance or a string describing a problem. Second, we have added code that checks the computed time of day, and returns an error string if the sum is out of bounds. To illustrate further how the informal exceptions work, we dene a method that calls the + method dened in this section. We dene a method, correct-arrival-time, that adds predicted weather and trafc delays to an arrival time; and we dene say-corrected-time, which calls correct-arrival-time and displays the results:
define method correct-arrival-time (arrival-time :: <time-of-day>, weather-delay :: <time-offset>, traffic-delay :: <time-offset>)
6.3. Exceptions
219
=> (sum :: type-union(<time-of-day>, <string>)) let sum1 = weather-delay + arrival-time; // Check whether the result of + was a string representing an error if (instance?(sum1, <string>)) sum1; else // Otherwise, if there is no error, compute the second part of the sum traffic-delay + sum1; end if; end method correct-arrival-time; define constant $no-time = make(<time-offset>, total-seconds: 0); define method say-corrected-time (arrival-time :: <time-of-day>, #key weather-delay :: <time-offset> = $no-time, traffic-delay :: <time-offset> = $no-time) => () let result = correct-arrival-time(arrival-time, weather-delay, traffic-delay); // Check whether the result of + was a string representing an error if (instance?(result, <string>)) format-out("Error during time correction: %s", result); else // Otherwise, if there is no error, display the result say(result); end if; end method say-corrected-time;
Problems with the informal exception protocol There are several signicant problems with the approach used in The + method using informal exceptions: As we saw in the correct-arrival-time method, most callers of the + function must check the type of the value returned. This type checking breaks up the normal ow of control, and gives as much weight to the unusual case (the exception) as it does to the usual case. If a caller fails to check the return value to see whether that value is a string, then a different error will occur later in the program (such as adding a string and time together), when it might be hard to trace back the problem to the original point of failure. Note that both direct callers of + (correct-arrival-time) and indirect callers of + (say-corrected-time) must understand and use this error protocol correctly. For other methods that might return any object (including strings, for example), an additional return value would have to be used to indicate that an exception occurred. It would be easy to forget to check the extra return value and such failure could easily go undetected, causing unpredictable program behavior. If the method is being added to a generic function in another library, it might be impossible to add a second return value indicating failure, because the generic function might limit the number of return values. A casual reader of the code could become easily confused about this ad hoc error protocol. Someone might inadvertently write code that did not obey this ad hoc protocol. Also, if all programmers use their own error protocols, it will be hard to remember which convention to obey at the call site; programmers will have to check the convention in the source code or programmer documentation. In this example, the ability to restrict the return value to only <time-of-day> is lost. This loss might prevent compile-time error checking that could catch errors that would be difcult or inconvenient to catch at run time. It might also prevent the compiler from optimizing code that uses the results of this function, thus decreasing performance of the application.
220
We are limited in how we can respond to the error. The context in which the error was detected has been lost. There is no state we can examine to gather more details about the error, and to determine why the error occurred. We also cannot correct whatever caused the problem, then continue from the point where the error occurred.
6.3. Exceptions
221
say(condition.min-valid-time); format-out(" and must be less than "); say(condition.valid-time-limit); format-out("."); end method say;
We redene the + method to signal the <time-boundary-error> condition (instead of returning an error string) to indicate that this problem has occurred:
define method \+ (offset :: <time-offset>, time-of-day :: <time-of-day>) => (sum :: <time-of-day>) let sum = make(<time-of-day>, total-seconds: offset.total-seconds + time-of-day.total-seconds); if (sum >= $midnight & sum < $tomorrow) sum; else error(make(<time-boundary-error>, invalid-time: sum, min-time: $midnight, time-limit: $tomorrow)); end if; end method \+;
We create the condition with make, just as we create instances of other classes. We call the error function to signal the condition. The error function is guaranteed never to return to its caller. Now we can specify an exact return value for the + method, because we are no longer returning an error string to indicate a problem with the addition. In previous chapters (for example, in Method for adding other kinds of times), we called the error function with a string. Given a string as its rst argument, the error function creates a general-purpose condition named <simple-error> and stores its arguments in the condition instance. In the preceding example, however, we created an instance of a condition that is customized for our program (<time-boundary-error>), and then supplied that condition to the error function. This approach provides information that is more readily accessible to the code that will handle the condition. Conditions, like any other Dylan class, can use inheritance, and can participate in generic function dispatch. For example, we dene say methods for our errors, so that our handlers can provide a reasonable error message to the user. (Unfortunately, Dylan debuggers do not yet have a standard way to know about our say generic function. We expect that Dylan will eventually support such a mechanism.) Supplying a specic condition to the error function brings the full power of Dylans object-oriented programming capabilities to the task of signaling and handling exceptional situations. Once the error function receives a condition instance, or makes an instance of <simple-error> itself, Dylan begins a process of attempting to resolve the situation represented by the condition. We present the details of condition resolution in the next section. Simple condition handling A handler can potentially resolve an exceptional situation, although a handler can decline to resolve a particular exception. If an application provides no handlers, then the generic function default-handler is called on the condition. There is a method on <condition> that just returns false, and there is a method on <serious-condition> (a superclass of <error>) that causes some kind of implementation-specic response to be invoked. Most development environments provide a debugger that deals with any serious conditions not handled by the application. Typically, the debugger describes the serious condition being signaled, and might provide any number of options for recovery (or might provide no recovery options). In a sense, the debugger is the handler of nal resort. In the following example, we establish a handler for the condition that we want to resolve, before calling the code that might signal that condition. We redene the correct-arrival-time and say-corrected-time methods to 222 Chapter 6. Part 4. Advanced Topics
The exception clause of block establishes a handler for a condition, and all that conditions subclasses, for any code in the block body, and for any code called by the block body. We say that the handler is established within the dynamic scope of the block body. When an exception is signaled, Dylan starts a search to nd the nearest handler available that matches the condition signaled, and that accepts the exception. The nearest handler is the one that was most recently established in the dynamic scope of the signaler. The handler matches the condition if the class associated with the handler (the handler class) is the same as the condition, or if the handler class is a superclass of the condition. You can associate a test with the handler so that the handler can selectively accept the condition. By default, a matching handler always accepts. If a handler established by the exception clause of block matches and accepts, then a nonlocal exit from the signaler occurs, with execution continuing in the body of the exception clause, which is executed in the context of the very beginning of the block. All the locals dened by the block are gone, but the exit procedure (if there is one) is still available. If there is relevant local state, it may be captured in slots of the condition prior to signaling of the condition. The code within the exception clause body is executed, and the value of the last statement in that body is then returned as the value of the block. In this example, the + method (called by correct-arrival-time) may signal a <time-boundary-error> condition using the error function during the execution of say-corrected-time. If this error is signaled, then the handler established by the block for <time-error> will match the <time-boundary-error> condition. This exception clause will always accept the condition, so a nonlocal exit will occur, and will terminate execution of the error function, the + method, and the correct-arrival-time method. Within the context of the beginning of the block, the variable condition is bound to the condition instance being signaled (the instance supplied to error; then, execution resumes with the code inside the body of the exception clause. The body calls the say generic function on the condition instance, which causes an appropriate error message (instead of the time) to be displayed to the user. Execution then continues normally after the end of the block; in this case, that results in the normal exit from the say-corrected-time method. Context transition from signaler to handler. shows the state of execution when error is called, and after the execution of the exception clause body for <time-error> begins. Context transition from signaler to handler. is a simplied diagram of the internal calling stack of a hypothetical Dylan implementation. It is similar to what a debugger might produce when asked to print a backtrace at these two points in the execution of the example. The error function called within the + method signals the <time-boundary-error> error, and the exception clause of block in the say-corrected-time method establishes the handler for that error. Once the handling of the exception is in progress, the handler selected is no longer established. If there is relevant local state, it may be captured in slots of the condition being signaled. The advantages of this structured approach to signaling and handling conditions are signicant: The method focuses on the normal ow of control, and the exceptional ow of control appears only where necessary. For example, the correct-arrival-time method does not need to be aware of the potential 6.3. Exceptions 223
Figure 6.4: Context transition from signaler to handler. exceptions at all. The Dylan condition system makes it easier to reuse code that might not know about, or care to participate in, your application-specic exception recovery code. Because correct-arrival-time does not need to participate in the exception-recovery protocol, it can also have a specic return value; thus, like the + method, it might allow better compiler optimizations and better compile-time error checking. We allow room for expansion in the code. For example, at some point, correct-arrival-time might do more sophisticated computations with time, which might signal other kinds of time errors. As long as these new time errors inherit from <time-error>, they can be resolved by the same handler established by say-corrected-time. As the application evolves, we can build various families of error conditions, and can provide application-specic handlers that perform the correct recovery actions for those families. Because we are using the signaling and handling protocol dened by Dylan, casual readers of the code should be able to understand our intent. Because the handler has access to the condition object, the handler can perform intelligent recovery actions based on the information captured in the condition object when the exception occurred. For example, the handler may examine various slots of the condition object, and perform different actions based on information stored in those slots. Dylan supports two models of handler execution. The exception clause of block implements the exit model. When you establish handlers by the exception clause of block, you do not have the ability to restart a computation in the context of the signaler, or in a context closer to the signaler than the handler. In Denition of a recovery protocol, we explore the calling model of handler execution, which allows you to recover from an exception without a nonlocal exit back to the point where the handler was established. Denition of a recovery protocol With the new denition of our + method on <time-offset> and <time-of-day>, if we add 5 hours to 10:00 P.M., a condition instance is signaled. The say-corrected-time method handles that condition, and prints a suitable error message. By the time the handler in say-corrected-time takes control, the addition that we
224
were performing has been aborted. In fact, we are no longer even executing within the correct-arrival-time method. We have ceased executing there because handlers established using the exception clause of block perform nonlocal exits out of the current computation back to the block where the handler was established. Suppose that we, instead of aborting the addition, wanted to continue with the addition, perhaps modifying the value returned by the + method such that it would still be within the correct 24-hour range for <time-of-day> instances. In this section, we modify say-corrected-time to use a different technique for establishing a handler that does not abort the computation in progress, and we modify the + method for <time-offset> and <time-of-day> to offer and implement a way to modify the value returned to be a legal time of day. First, we must nd a way to execute a handler in the context of the signaler, instead of at the point where the handler was established. Then, we must nd a way to activate special code in the + method to return a legal <time-of-day> instance as a way of recovering from the time-boundary exception. The let handler local declaration provides a way to establish a handler that will execute in the context of the signaler, just as though the handler was invoked with a normal function call by the signaler. The restart protocol provides a structured way for a handler to recover from the exception, and to continue with the computation in progress. In this case, continuing with the computation means that the + method will return a legal <time-of-day> instance to correct-arrival-time, and correct-arrival-time will nish any additional processing and return normally to its caller. To recover from an exception, we use a signaling and handling technique as similar to that we used to indicate the exception in the rst place. This time, we signal a particular condition that is a subclass of <restart>, to indicate how the exception handler wishes to recover. We use a restart handler to implement the particular recovery action. You can think of a restart as a special condition that represents an opportunity to recover from an exception. Establishing a restart handler is a way to offer such an opportunity to other handlers, and to specify the implementation of the restart. Any handler, when activated, might signal a restart to request that a particular recovery action take place. Restart signaling and handling connects recovery requests with recovery actions. For example, adding 5 hours to 10:00 P.M. is an error for <time-offset> and <time-of-day> instances. One way to recover from this error would be to wrap around the result to 3:00 A.M. Here, we dene the restart class <return-modulus-restart>, which represents an offer to return from a time-of-day computation by wrapping the result:
define class <return-modulus-restart> (<restart>) end class <return-modulus-restart>;
Using the exception clause of block, we redene the + method to establish and implement the restart handler:
define constant $seconds-per-day = $hours-per-day * $seconds-per-hour; define method \+ (offset :: <time-offset>, time-of-day :: <time-of-day>) => (sum :: <time-of-day>) let sum = make(<time-of-day>, total-seconds: offset.total-seconds + time-of-day.total-seconds); block () if (sum >= $midnight & sum < $tomorrow) sum; else error(make(<time-boundary-error>, invalid-time: sum, min-time: $midnight, time-limit: $tomorrow)); end if; // Establish restart handler exception (restart :: <return-modulus-restart>) make(<time-of-day>,
6.3. Exceptions
225
If a handler (established with let handler) signals a <return-modulus-restart> during the handling of the <time-boundary-error> exception, then the sum will be wrapped around so that it will stay within the bounds of the time-of-day specication, and the result will be returned from the + method. Next, we want to write a handler using let handler that will invoke the restart. However, before we invoke the restart, we want to conrm that the restart is currently established. Signaling a restart that is not currently established is an error. The available-restart method that follows returns an instance of a given restart, if that restart is currently established; otherwise, available-restart returns false:
define method available-restart (restart-class :: <class>, exception-instance :: <condition>) => (result :: false-or(<restart>)) block (return) local method check-restart (type, test, function, initargs) // Make an instance of the restart, so we can see whether it matches // our search criteria if (subtype?(type, restart-class)) let instance = apply(make, type, condition:, exception-instance, initargs | #[]); if (test(instance)) return(instance); end; end if; end method; // The built-in Dylan function do-handlers will call check-restart // for every handler currently established, in order (first is nearest // to the signaler) do-handlers(check-restart); #f; end block; end method available-restart;
Dylan provides the do-handlers function, which iterates over all the currently established handlers, calling its argument (a function) on all the relevant information about the handler, including all the information necessary to instantiate a restart instance for restart handlers. The check-restart local method returns from available-restart with a restart instance only when a matching restart that accepts is found. All restarts take a condition initkeyword argument, which, if supplied, should be the original exception that occurred. If the handler that created the restart provided the original exception condition as an init-keyword argument, then restart handlers can handle restart conditions for only particular exceptions. If none of the established handlers match and accept the restart that we seek, then available-restart returns false. Note that you should establish restart handlers for instantiable restart classes only, because the restart classes will be instantiated by restart-savvy handlers. If the restart classes cannot be instantiated, then the recovery process will not operate correctly. Next, we need to dene a method to be called by the exception handler to invoke the restart whether it is available. If the restart is not available, the method will call the next-handler method, which will allow another handler the opportunity to decide if it will handle the exception. In other words, if the <return-modulus-restart> restart is not established, the handler for <time-error> established by say-corrected-time will decline to handle the <time-boundary-error> condition being signaled.
define method invoke-modulus-restart-if-available (condition :: <time-error>, next-handler :: <function>) let restart = available-restart(<return-modulus-restart>, condition); if (restart) error(restart); else next-handler(); end; end method invoke-modulus-restart-if-available;
226
what next-handler might return. Our handler method must be prepared to return any number of objects of any types. Next, we establish a handler using the let handler local declaration:
define method say-corrected-time (arrival-time :: <time-of-day>, #key weather-delay :: <time-offset> = $no-time, traffic-delay :: <time-offset> = $no-time) => () let handler (<time-error>) = invoke-modulus-restart-if-available; say(correct-arrival-time(arrival-time, weather-delay, traffic-delay)); end method say-corrected-time;
The let handler local declaration establishes a handler for the <time-error> condition and for all that conditions subclasses. When the error function inside the + method signals the <time-boundary-error> condition instance, Dylan conducts a search for the nearest matching handler that accepts. In this case, the nearest matching handler that accepts is the handler established by say-corrected-time. Because this handler was established by a let handler local declaration, instead of by the exception clause of block, no nonlocal exit takes place. Instead, the function specied in the let handler local declaration is invoked in the context of the signaler. The error function essentially performs a regular function call on the function associated with the nearest matching handler. The function is passed the condition instance being signaled, and the next-handler function that might be used to decline handling this condition. In our example, the invoke-modulus-restart-if-available function will be called from error. Once called, invoke-modulus-restart-if-available will rst see whether the <return-modulus-restart> restart is established. If the restart is established, we will invoke it by signaling an instance of the restart. If the restart is not established, we decline to process the <time-boundary-error> condition in this handler. Assuming that no other handlers exist, the debugger will be invoked. If the restart is signaled, a nonlocal exit to the restart exception clause in + method is initiated, which returns the sum suitably wrapped such that it lies within the 24-hour boundary. Context transition from handler to restart handler. shows the state of execution after the handler function for <time-error> is invoked, and the state after the restart handler function for <return-modulus-restart> is invoked. As you can see, although establishing a handler with let handler can be far removed from the signaler, the handler function itself is executed in the context of the signaler. Continuation from errors The restart mechanism just described is exceedingly general, and may provide several different ways to recover from exceptional situations. Sometimes, however, there is just one main way to recover. Under certain circumstances, Dylan provides a way for handlers simply to return to their callers, allowing execution to continue after the signaler. Here, we present a simpler (but less exible) implementation for recovering from the time-of-day overow exception:
define method return-24-hour-modulus (condition :: <time-error>, next-handler :: <function>) => (corrected-time :: <time>) make(type-for-copy(condition.invalid-time), total-seconds: modulo(condition.invalid-time.total-seconds, $seconds-per-day)); end method return-24-hour-modulus; define method return-allowed? (condition :: <time-error>) #t; end method return-allowed?; define method return-description (condition :: <time-error>) "Returns the invalid time modulo 24 hours."; end;
6.3. Exceptions
227
define method say-corrected-time (arrival-time :: <time-of-day>, #key weather-delay :: <time-offset> = $no-time, traffic-delay :: <time-offset> = $no-time) => () let handler (<time-error>) = return-24-hour-modulus; say(correct-arrival-time(arrival-time, weather-delay, traffic-delay)); end method say-corrected-time; define method \+ (offset :: <time-offset>, time-of-day :: <time-of-day>) => (sum :: <time-of-day>) let sum = make(<time-of-day>, total-seconds: offset.total-seconds + time-of-day.total-seconds); block () if (sum >= $midnight & sum < $tomorrow) sum; else // If a handler returns, it must return a valid <time-offset> signal(make(<time-boundary-error>, invalid-time: sum, min-time: $midnight, time-limit: $tomorrow)); end if; end block; end method \+;
The return-allowed? and return-description generic functions are provided by Dylan. When the generic function return-allowed? returns true for a given condition, introspective handlers know that they can return successfully back to the signaler. When returning is allowed, such introspective handlers may call the return-description generic function to nd out what values to return, if there are any. This description can be 228 Chapter 6. Part 4. Advanced Topics
especially useful for interactive handlers, such as debuggers. The return-24-hour-modulus method has been generalized compared to the exception-specic restart dened in Denition of a recovery protocol. This method may return either an instance of <time-of-day> or <time-offset>, depending on the class of time that overowed. Thus, it could be reused for exception handling in other parts of the application. In this implementation approach, there is an implicit contract between the signaler in the + method and any handler that matches and accepts <time-boundary-errors>. The contract is that the handler will always return a valid <time> value, or will never return at all. If any handler violates this implicit contract, then the reliability of the program will be placed at risk. It is important to document these error-handling contracts. Note that, in the + method, we must use the signal function to signal the exception, because it is illegal for a handler to return from exceptions signaled with the error function.
6.3. Exceptions
229
Each subclass of <protected-object> would inherit an object-lock slot. The lock instance stored in this slot must be acquired prior to any operation on the protected object, and released when the operation is complete. One naive way to implement call-using-lock would be as follows:
define method call-using-lock (object :: <protected-object>, function :: <function>, #rest args) => (#rest results) get-lock(object.object-lock); apply(function, object, args); release-lock(object.object-lock); end method call-using-lock;
The approach in the preceding example has two serious problems. First, call-using-lock does not return the values returned by calling function. Second, if function executes a nonlocal exit past call-using-lock, the release-lock call will never be executed, and after that point no process will be able to acquire the lock for the protected object. Thus, subsequent attempts to use the protected object will wait forever, because the lock was not properly released. We could add a handler that would release the lock if any condition is signaled, but that might be incorrect, because certain conditions might be handled within the dynamic scope of function, and might never perform a nonlocal exit past call-using-lock. Thus, the lock might be released prematurely, possibly causing the integrity of the protected object to be violated. Also, calling an exit procedure performs a nonlocal exit without signaling a condition at all. To solve exactly this sort of problem, Dylan provides the cleanup clause of block. Code within the body of a cleanup clause is guaranteed to be executed before the block is exited, even if it is a nonlocal exit that causes the block to terminate. The value of this block will be the result of calling function. The cleanup clause does not affect what the block returns.
define method call-using-lock (object :: <protected-object>, function :: <function>, #rest args) => (#rest results) block () get-lock(object.object-lock); apply(function, object, args); cleanup release-lock(object.object-lock); end block; end method call-using-lock;
The cleanup clause of block provides a powerful tool for ensuring the integrity of applications that use nonlocal exits.
6.3.5 Summary
In this chapter, we covered the following: We described how to dene condition classes, and to signal them. We explored establishing simple error handlers using the exception clause of block. We showed how to design and implement a introspective recovery protocol using let handler, do-handler, and restarts. We demonstrated how a handler can simply return to the signaler with cooperation from that signaler. 230 Chapter 6. Part 4. Advanced Topics
We showed how we can protect sections of code from unexpected nonlocal exits by using the cleanup clause provided by block. You can use these techniques to control the handling of exceptional situations when they arise. By designing your condition classes carefully and handling those conditions correctly, you make your program signicantly more robust, without interrupting the normal ow of control. By providing recovery protocols, you make it possible to continue cleanly after a problem has been detected. By protecting critical code against unexpected nonlocal exits, you enhance the reliability of your applications.
6.4 Macros
The term macro, as used in computer programming, originally stood for macro-instruction, meaning an instruction that represented a sequence of several machine (or micro) instructions. Over time, the term has evolved to mean any word or phrase that stands for another phrase (usually longer, but built of simpler components). Macros can be used for abbreviation, abstraction, simplication, or structuring. Many application programs, such as word processors or spreadsheets, offer a macro language for writing scripts or subroutines that bundle a number of simpler actions into one command. Many computer languages support a macro facility for creating shorthand notations for commonly used, longer phrases. They range from simple, text-based abbreviations to full languages, permitting computed replacements. Macros are processed before the program is compiled by expanding each macro into its replacement phrase as that macro is encountered until there are no more macros. You can use macros to extend the base language by dening more sophisticated phrases in terms of simpler, built-in phrases. The primary use of macros in programming languages is to extend or adapt the language to allow a more concise or readable solution for a particular problem domain. A simple program rarely needs macros. More complicated programs, including the implementation of a Dylan compiler and run-time system, will use macros often. Macros have no visible run-time cost or effect they are transformations that take place during the compilation of a program (hence, they can increase compilation time). Although macros may take the form of function calls, they are not functions they cannot be passed as functional arguments, and they cannot be invoked in a run-time image as a function can. Although macros may have parameters, they do not take arguments the way functions do. The arguments to a macro are not evaluated; they are simply program phrases that can be substituted in the replacement phrase. Dylan provides a macro facility that is based on pattern matching and template substitution. This facility is more powerful than is a simple textual substitution facility, but is simpler than a procedural-macro facility, which allows arbitrary computations to construct replacement phrases. Dylans macro facility is closely integrated with the Dylan language syntax, and permits most macro needs to be satised. Dylan designers have also planned for a full procedural macro capability, so that it can be added compatibly at a later time if there is sufcient demand. Comparison with C and C++: C and C++ macros are text substitutions, performed by a preprocessor. The preprocessor has no understanding of the language; it simply splices together text fragments to create replacement phrases. Dylan macros are written in terms of Dylan language elements; the macros choose their transformation by pattern matching, and they substitute program fragments. Language-based macros are more powerful than and avoid a number of common pitfalls of text-substitution macros. These pitfalls are described in later comparisons in this chapter.
6.4. Macros
231
When a macro is invoked, each rule is tried in order until a matching pattern is found. When a match is found, the macro is replaced by the matching template. If no match can be found, an error occurs. Dylan macros are recognized by the compiler because they t one of three possible formats: the function macro, the statement macro, and the dening macro. The macro format determines the overall fragment that is matched against the macros rules at each macro invocation. The simplest macro format that the compiler can match is that of a function call. A function macro is invoked in exactly the same way that a function is invoked. The name of the macro is a module variable that can be used anywhere a function call can occur. Typically, it is simply the name followed by a parenthesized list of arguments, but recall that slot-style abbreviations and unary and binary operators are also function calls. The most important use of function macros is to rearrange or delay evaluation of arguments. The fragment that is matched against the function macros rules is the phrase that represents a functions arguments. The function macro can then rearrange the function arguments, perhaps adding code. When a macro rearranges its arguments, its action has the effect of delaying the evaluation of the arguments (as opposed to a function call, where the argument expressions are evaluated and then passed to the function). One simple use of delaying evaluation is to write a function-like construct similar in spirit to Cs ?: operator:
define macro if-else { if-else (?test:expression, ?true:expression, ?false:expression) } => { if (?test) ?true else ?false end } end macro if-else;
We could not write if-else as a function, because both the true and false expressions would be evaluated before the function was even called:
? define variable *x* = 0; ? define variable *y* = 0; ? *y* := if-else(*y* == 0, *x* := 1, *x* := -1); => 1 ? *y*; => 1 ? *x*; => 1
If we had dened if-else as a function, *x* would have been -1, rather than 1, because both assignments to *x* would have been evaluated, before if-else was called. When a macro is used, the assignments are just substituted into the template if, which evaluates the rst clause only when the condition is true. Looking at the macro denition of if-else, we can infer basic ideas about macros. A macro is introduced by define macro, followed by the macro name in this case, if-else. The denition of the macro is a rule that has two parts: a pattern enclosed in braces, {}, that mimics the fragment that it is to match, and a replacement. Macro parameters, called pattern variables, are introduced in the pattern by ?. They match fragments with particular constraints in this case, :expression. They are delimited by punctuation in this case, the open and close parentheses, (), and the comma, ,. The replacement part of the rule, the expansion, is indicated by => and is dened by a template, also enclosed in braces. The template is in the form of a code fragment, where pattern variables are used to substitute in the fragments they matched in the pattern. Note that matching and replacement are language based, so required and optional whitespace is treated exactly as in Dylan. We have used optional whitespace to improve the legibility of the macro denitions presented here. Most Dylan development environments provide a way to view code after all macros have been expanded. This view can be helpful in debugging macros that you write. For example, showing the expanded view of an expression like
232
might yield
*y* := if (*y* == 0) *x* := 1 else *x* := -1 end;
The exact format of the expanded view of the macro depends on the particular development environment. Here, we show the code that comes from the macro template in underlined italic, whereas the fragments matched by the pattern variables and substituted into the template are presented in our conventional code font. Note that the if-else macro we have dened is just syntactic sugar Dylans built-in if statement is perfectly sufcient for the job. Another reason to delay evaluation is to change the value of an argument for example, to implement an operator similar in spirit to Cs ++ and += operators:
define macro inc! { inc! (?place:expression, ?by:expression) } => { ?place := ?place + ?by; } { inc! (?place:expression) } => { ?place := ?place + 1; } end macro inc!;
In this macro, it is important to delay the evaluation of the rst argument because we want to be able to assign to the variable or slot it is stored in, rather than simply to manipulate the value of the variable or slot. The inc! macro demonstrates the use of multiple rules in a macro. They are tried in order until an appropriate match is found. This allows the inc! macro to have two forms. The one-argument form increments the argument by 1. The two-argument form allows the increment amount to be specied.
6.4. Macros
233
The local variable value is created by the macro. There is a possibility that this variable could conict with another variable in the surrounding code. Consider what might happen if we were to expand swap!(value, x):
let value = value; value := x; x := value
With simple textual substitutions, swap! would have no effect in this case. Dylans hygienic macros solve this problem by differentiating between the value introduced by the macro and any other value that might appear in the original code. Comparison with C: Because C (and C++) macros are simply text substitutions performed by a preprocessor that has no understanding of the C language, they are inherently unhygienic. C macro writers reduce this problem by choosing unusual or unlikely names for local variables in their macros (such as _swap_temp_value), but even this workaround can be insufcient in complex macros. Dylan macros in effect automatically rename macro variables on each expansion to guarantee unique names.
When a function is invoked, all its arguments are evaluated rst, which defeats our purpose. If we model our macro on our function idea, however, we will not get the ideal result either:
define macro or-int { or-int (?arg1:expression, ?arg2:expression) } => { if (?arg1 ~= 0) ?arg1 else ?arg2 end } end macro or-int;
We see a common macro error the expression x := x + 1 will be evaluated twice when the resulting substitution is evaluated, leaving x with an incorrect (or at least unexpected) value. There is no magic technique for avoiding this error you just have to be careful about repeating a pattern variable in a template. Most often, if you are repeating a pattern variable, you should be using a local variable instead, so that the fragment that the pattern represents is evaluated only once:
define macro or-int { or-int (?arg1:expression, ?arg2:expression) } => { let arg1 = ?arg1; if(arg1 ~= 0) arg1 else ?arg2 end
234
Another potential pitfall arises if the pattern variables appear in an order in the template different from the one in which they appear in the pattern. In this case, unexpected results can occur if a side effect in one fragment affects the meaning of other fragments. In this case, you would again want to use local variables to ensure that the fragments were evaluated in their natural order. These rules are not hard and fast: The power of macros is due in a large part to the ability of macros to manipulate code fragments without evaluating those fragments, but that power must be used judiciously. If you are designing macros for use by other people, those people may expect function-like behavior, and may be surprised if there are multiple or out-of-order evaluations of macro parameters. Comparison with C: Because it is more difcult to introduce local variables in C macros than it is in Dylan macros, most C programmers simply adopt the discipline of never using an expression with side effects as an argument to a macro. The problem of multiple or out-of-order evaluations of macro parameters is inherent in all macro systems, although some macro systems make it easier to handle.
6.4.4 Constraints
So far, in our macros, we have seen the constraint expression used for the pattern variables. Except for a few unusual cases, pattern variables must always have a constraint associated with them. Constraints serve two purposes: they limit the fragment that the pattern variable will match, and they dene the meaning of the pattern variable when it is substituted. As an example, consider the following statement macro, which we might nd useful for manipulating the decoded parts of seconds:
define macro with-decoded-seconds { with-decoded-seconds (?max:variable, ?min:variable, ?sec:variable = ?time:expression) ?:body end } => { let (?max, ?min, ?sec) = decode-total-seconds(?time); ?body } end macro;
A statement macro can appear anywhere that a begin / end; block can appear. A statement macro introduces a new begin word in this case, with-decoded-seconds and is matched against a fragment that extends up to the matching end. The pattern and the constraints on the pattern variables limit what the macro will match; they dene the syntax of
6.4. Macros
235
this particular statement. In the case of with-decoded-seconds, the syntax of this statement begins with a parenthesized list of Three variable expressions (that is, name :: The literal token = An expression (any Dylan expression yielding a value) After the parenthesized list comes a body (any sequence of expressions separated by ;, just as would be valid in a begin / end; block). Note the use of the abbreviation ?:body, to mean ?body:body (a pattern variable, body, with the constraint body). The constraints are similar to type declarations on variables: They limit the acceptable values of the pattern variables, and they help to document the interface of the macro. The constraints also serve a second purpose: Once the compiler has recognized a fragment under a particular constraint, it will ensure the correct behavior of that fragment when that fragment is substituted in a template. For example, suppose that we dene a function macro:
define macro times { times (?arg1:expression, ?arg2:expression ) } => { ?arg1 * ?arg2 } end macro times;
We can see that, if the macro were a simple text-substitution macro, the result would be 12, rather than the 28 we were expecting. But because, in Dylan, the constraint is maintained when a pattern variable is substituted (that is, the expression that makes up each of the pattern variables remains a single expression), the result is as though the macro automatically inserted parentheses, and the expansion were
(1 + 3) * (2 + 5)
Some development environments may display the implicit parentheses of an expression constraint. Thus, the macro will yield the expected result of 28. Comparison with C: Because C macros are simple textual substitutions, the macro writer must be sure to insert parentheses around every macro variable when it is substituted, and around the macro expansion itself, to prevent the resulting expansion from taking on new meanings.
236
=> { } { ?flight; ... } => { ?flight, ... } flight: { flight ?id:name, #rest ?options:expression } => { make(<flight>, id: ?#"id", ?options) } end macro aircraft-definer;
This macro shows a number of the more esoteric features of Dylan macros. First, notice the pattern variable ?flights, which has no constraint, but rather is called out as an auxiliary rule. When the compiler matches this macro, it will try each of the auxiliary rules patterns listed under flights: for a match. When it nds a match, it will assign the pattern variable ?flights to the fragment resulting from the matching patterns template substitution. In effect, auxiliary rules give a way of writing new constraints, combined with the effect of a subroutine for matching and substitution. In this particular case, we use the auxiliary rule to map yet another auxiliary rule, flight, over a sequence of ight descriptions that look similar to the slot descriptions in a class. The mapping is signaled by the points of ellipsis (...) which means that the rule should be applied recursively (that is, the current rule is matched again to the fragment that matches ...). Note that flights must have a rule to cover the case of there being no ight; that rule also handles the end of the recursion when the nal ight has been matched. The flight rule simply converts each ight name and its options into the appropriate call to make, to create the ight. We could extend this rule to allow a more natural specication for ight origin, destination, and time. We do the work of dening an aircraft by calling the helper functions register-aircraft and register-flights (which are not given here), but the macro takes care of getting the arguments in order. The substitution "<" ## ?type ## ">" turns the name DC10 into the name <DC10> by using concatenation, allowing a more concise format for our dener while maintaining our convention for naming types. The substitution ?#"identifier" turns the name UA1306 into the symbol #"UA1306" by using coercion; the program can use the symbol #"UA1306" to look up an aircraft in the registry by name. The template for flights collects all the individual ights into a comma-separated list that is passed to register-flights as a #rest argument.
6.4. Macros
237
=> { block (?=stop!) local method again() ?body; again() end; again(); end } end macro repeat;
The term ?=stop! says that the local variable stop!, which is the block exit variable, will be visible when the macro is called exactly as stop!; there will be no hygienic renaming. Here is an example that uses the macro to count to 100:
begin let i = 0; repeat if (i == 100) stop!() end; i := i + 1; end; end;
Note that the body constraint invokes the Dylan parser to match the code properly between the repeat and the corresponding end. It is not confused by the end of the if statement, as a text-based macro might be. The expanded view of the preceding code might look like this:
begin let i = 0; block (stop!) local method again() if (i == 100) stop!() end; i := i + 1; again() end; again(); end; end;
Note that we have shown the local variable stop! introduced by the macro block in code font rather than in underline italic, because it is visible to the body and is exactly the stop! called in the if to stop the repetition. The local variable again, on the other hand, is not visible to the body code. We could use again instead of i as our repetition count without a problem. Comparison with C: All C macros have the syntax of function calls, making it impossible to write language extensions such as repeat. By using language-based constraints, such as the body constraint used here, Dylan macros can match language forms, and thus can create extensions that are consistent with the base language. Note that we would have to document how repeat works for other users, or they might be surprised if they tried to use stop! instead of i in the example.
238
define macro aircraft-definer { define aircraft ?identifier:name (?type:name) ?flights end } => { register-aircraft(make("<" ## ?type ## ">", id: ?#"identifier")); register-flights(?#"identifier", ?flights) } flights: { } => { } { ?flight; ... } => { ?flight, ... } flight: { } => { } { flight ?id:name, #rest ?options:expression } => { make(<flight>, equipment: ?"type", id: ?#"id", ?options) } end macro aircraft-definer;
When we are processing the flight auxiliary rules, we would like to be able to reference the pattern variable ?type (coercing it to a string) from the main rules, but it is not in scope it is inaccessible to the auxiliary rules. We could have register-flights set the equipment slot after the ight is created, but we would prefer to initialize the slot at the time we create the <flight> object. There is a workaround, an auxiliary macro:
define macro aircraft-definer { define aircraft ?identifier:name (?type:name) ?flights:* end } => { register-aircraft (make("<" ## ?type ## ">", id: ?#"identifier")); define flights (?#"identifier", ?"type") ?flights end } end macro aircraft-definer; define macro flights-definer { define flights (?craft:name, ?equipment:name) end } => { } { define flights (?craft:name, ?equipment:name) ?flight ; ?more:* end } => { register-flights (?craft, make(<flight>, equipment: ?equipment, ?flight)) ; define flights (?craft, ?equipment) ?more end } flight: { } => { } { flight ?id:name, #rest ?options:expression } => { id: ?#"id", ?options } end macro flights-definer;
Here, we have essentially broken out the work that used to be done by the auxiliary rule flights into a separate denition macro. Where flights used points of ellipsis to walk over each ight, the denition macro uses a wildcard constraint ?more:*, explicitly calling itself again (that is, the macro appears in the substitution, and will be expanded again), as long as there are more ights to be processed. Here is an example use of the flights-definer macro:
define aircraft UA4906H (DC10) flight UA11 from: #"BOS", to: #"SFO"; flight UA12 from: #"SFO", to: #"BOS"; end aircraft UA4906H;
6.4. Macros
239
register-aircraft (make(<DC10>, #"UA4096H")); register-flights (#"UA4096H", make(<flight>, equipment: "DC10", id: #"UA11" from: #"BOS", to: #"SFO"); register-flights (#"UA4096H", make(<flight>, equipment: "DC10", id: #"UA12" from: #"SFO", to: #"BOS");
(Note that this example is a hypothetical one used to illustrate macro expansion. The define aircraft statement cannot be compiled in the airport example.)
6.4.8 Summary
In this chapter, we introduced macros by explaining their purpose as a language-extension tool, and by showing a range of Dylan macros. Macros can be useful when you want to tailor the language to express a particular problem domain more concisely. Pattern constraints. summarizes how constraints control pattern-variable matches. Table 6.4: Pattern constraints. ConMatches straint token a lexeme (a Dylan word), including literal strings, symbols, and numbers and punctuation name a Dylan identier, including reserved identiers, such as define, end, and operators such as +, or * variableeither variable or variable :: <type>, useful for macros that mimic variable binding (automatically drops the :: <type>, as appropriate on substitution) expression a well-formed Dylan expression a constant, such as 37; a variable, such as *my-position*; a function call, such as get-current-time(); a statement, such as if (test) 12 else try() end; or a binary operand series, such as x + y * z body a well-formed Dylan body a sequence of semicolon-separated constituents, each constituent being either a denition, local declaration, or expression case-body Dylan case statement body a any sequence of Dylan tokens and parsed forms * Multiple Inheritance, describes how multiple inheritance works in Dylan. It describes how method dispatch is affected by multiple inheritance. It gives an example of using the mix-in style of designing classes with multiple inheritance. Performance and Flexibility, describes the fundamental tradeoff between performance and exibility. You can take advantage of Dylans dynamic nature during the initial stages of development. Later on, when your application is nearing completion, you can optimize the performance of the program (and sacrice exibility, which presumably is no longer needed). Exceptions, describes how to use Dylan facilities to help create reliable programs in the face of exceptions unexpected events that occur during program execution. Macros, describes how to dene macros in Dylan. Macros can be used for abbreviation, abstraction, simplication, or structuring. They are also useful for delaying evaluation of arguments.
240
CHAPTER
SEVEN
241
position.dylan say-library.dylan say.dylan say.lid schedule.dylan sixty-unit-library.dylan sixty-unit.dylan sixty-unit.lid sorted-sequence-library.dylan sorted-sequence.dylan sorted-sequence.lid time-library.dylan time.dylan time.lid vehicle-dynamics.dylan
242
CHAPTER
EIGHT
RESOURCES ON DYLAN
8.1 World Wide Web pages for this book and its examples
Both Addison-Wesley and Harlequin maintain Web pages about this book, including the source code of the program examples, and excerpts from the book. The address of the Addison-Wesley Web page for computer science and engineering is
8.1.1 http://www.aw.com/cseng/
The address of Harlequins Web page is
8.1.2 http://www.harlequin.com/
8.2 Newsgroup
The name of the newsgroup about Dylan is
8.2.1 comp.lang.dylan
The newsgroup is also available as an Internet mailing list. To subscribe to the info-dylan mailing list, send a message to [email protected]. In the body of the message, include the words subscribe info-dylan. You may subscribe to the Dylan announcements mailing list by including the words subscribe announce-dylan.
8.3 Harlequin
Harlequins initial Dylan product is a Dylan implementation for Windows 95 and Windows/NT. Harlequin will offer Dylan implementations on the three major platforms: Windows, UNIX, and the Macintosh. Harlequin will provide a native Dylan development environment and technical support. Harlequins Dylan Web site contains a great deal of useful information about Dylan, including the FAQ, The Dylan Reference Manual, and pointers to public-domain implementations of Dylan and to the comp.lang.dylan newsgroup. The address of Harlequins Dylan Web page is
243
8.3.1 http://www.harlequin.com/full/dylan.html
Harlequins main Web site is located at
8.3.2 http://www.harlequin.com/
Harlequins main telephone numbers are United States: 617-374-2400 United Kingdom: 44 (0) 1223 873800 Harlequins main addresses are Harlequin Incorporated One Cambridge Center Cambridge, MA 02142 USA Harlequin Limited Barrington Hall Barrington Cambridge United Kingdom CB2 5RG
8.4.1 http://legend.gwydion.cs.cmu.edu/gwydion/
The address of Carnegie Mellons Gwydion and Dylan ftp site is
8.4.2 ftp://legend.gwydion.cs.cmu.edu/usr/gwydion/ftp/
Carnegie Mellons main Web site is located at
8.4.3 http://www.cmu.edu/
Carnegie Mellons electronic mail address for Gwydion is
244
8.4.4 [email protected]
8.5.1 http://www.cambridge.apple.com/
Apples Dylan ftp site is located at
8.5.2 ftp://ftp.cambridge.apple.com/pub/dylan/
You can order the Apple Dylan Technology Release from the Apple Developer Catalog Online, located at
8.5.3 http://www.devcatalog.apple.com/
The telephone numbers for the Apple Developer Catalog Online are U.S.: 1-800-282-2732 Canada: 1-800-637-0029 International: 1-716-871-6555 Apples main Web site is located at
8.5.4 http://www.apple.com/
Apples main address is Apple Computer, Inc. 1 Innite Loop Cupertino, CA 95014
245
8.6.1 http://www.digitool.com/
Digitools telephone number is 617-441-5000 Digitools address is Digitool, Inc. One Main Street 7th Floor Cambridge, MA 02142
8.7 Marlais
Marlais is an experimental Dylan interpreter in the public domain as copylefted software. Marlais is available on UNIX, the Macintosh, and Windows. It was originally developed by Brent Benson of Harris Computer Systems, and new versions were developed by Joseph N. Wilson, at the University of Florida. Patrick Beard developed the Macintosh implementation of Marlais. The address of the Web site for Marlais is
8.7.1 http://www.cise.u.edu:/~jnw/Marlais/
The Web site for the Computer and Information Science and Engineering Department of the University of Florida is located at
8.7.2 http://www.cise.u.edu/
The telephone number of the department where Marlais is being developed is 904-392-1200 The address of the department where Marlais is being developed is Computer & Information Science & Engineering Room E301 CSE Building PO Box 116120 University of Florida Gainesville, FL 32611-6120
246
CHAPTER
NINE
247
The benet of the Dylan model is that the nal two statements are a single pointer assignment and a passing of a single pointer as a parameter. The comparison in whitenessTest is a single pointer comparison. Another possible C implementation one more typical of C style, but not equivalent to the Dylan implementation is as follows: C-style example, without pointers.
typedef struct _color { int red, green, blue; } Color; Color const black = {0, 0, 0}; Color const white = {16777215, 16777215, 16777215}; void whitenessTest(Color const color) { if (color.red == white.red && color.green == white.green && color.blue == white.blue) { printf("Its white!\n"); } } void main () { Color color = black; color = white; whitenessTest(color); }
In the C-style example, without pointers, the nal two statements consist of three integer assignments (as the Color structure is copied), and a passing of a three-slot structure (the equivalent of three arguments) as an argument. The comparison in whitenessTest is three integer comparisons (as the two Color structures are compared, slot by slot). The drawback of the Dylan object example is shown here:
248
color.blue := 0;
The preceding call makes white yellow! In the C-style example, without pointers, you would make only color yellow. You can prevent people from changing dened colors to other colors in Dylan by not allowing the slots of <color> objects to be modied once they are initialized in other words, by making <color> objects immutable: Dylan object example, with immutable objects.
define class <color> (<object>) constant slot red :: <integer> = 0, init-keyword: red:; constant slot green :: <integer> = 0, init-keyword: green:; constant slot blue :: <integer> = 0, init-keyword: blue:; end class <color>; define constant black = make(<color>); define constant white = make(<color>, red: 2 ^ 24 - 1, green: 2 ^ 24 - 1, blue: 2 ^ 24 - 1); define variable color = black; define method whiteness-test(color :: <color>) if (color = white) format-out("Its white!\n") end; end method whiteness-test; color := white; whiteness-test(color);
You can consider Dylan as always using pointers, even to objects such as integers and characters. Integers and characters are, by denition, immutable objects: There are no slots that you can change in an integer or character object. Thus, there is no danger of setting 6 to 9. Built-in immutable objects can have their pointers optimized away by the compiler: The compiler just has to arrange that 6 = 6 and 9 = 9, whether there is only one 6 object pointed to by all the variables with the value 6, or copies of 6 are stored in each of those variables (saving the need for a pointer). Another difculty in the Dylan model is this potentially embarrassing situation:
color := make(<color>, red: 2 ^ 24 - 1, green: 2 ^ 24 - 1, blue: 2 ^ 24 - 1); if (color = white) format-out("Its white!\n") end;
The preceding expression might not say Its white!, because make might return a new object with white RGB values, and that object would not be = to the object named white. The equivalent C code would be:
Color* make_color(int r, int g, int b) { Color* c = (Color*)malloc(sizeof(Color)); c->red = r; c->green = g; c->blue = b; return c; } static Color _white = {16777215, 16777215, 16777215}; Color* const white = &_white; Color* color = make_color(16777215, 16777215, 16777215); if (color == white) { printf("Its white!\n"); };
Because the preceding code is comparing the pointer stored in white to the pointer stored in color, it will clearly not say Its white!. The default implementation of = in Dylan is to compare pointers. There are several solutions to this difculty in Dylan. One is to customize the = comparison operator for our class to do a comparison more thorough than the default comparison:
249
define method \= (o1 :: <color>, o2 :: <color>) o1.red = o2.red & o1.green = o2.green & o1.blue = o2.blue; end method \=;
Now, using = will compare colors by checking their individual RGB components, and our whiteness test will work. Note that Dylan also provides the == comparison operator, which always compares pointers. This comparison is useful when you want to check object identity. But, as we have seen, it is not always the appropriate default for comparison of equality of objects. The compiler can avoid calling our = method altogether if the same object is compared to itself. It can do so because, with the exception of IEEE NaNs (nonnumbers), values that are == must also be =. Another approach that you can use if your objects are immutable is to make sure that they are unique. The make function is not required to return a new object each time, as shown in the Dylan object example, with unique, immutable objects. This advanced use of make and tables ensures that there is always only one instance of each color. Thus, when we make another white, it will always be the white, and our whiteness test will work with the default = comparison. The choice of solution depends on whether you will be doing more making or more comparing. Dylan object example, with unique, immutable objects.
define class <color-table> (<table>) end class <color-table>; define method table-protocol(<color-table>) local method color-hash(color :: <color>) let (red-id, red-state) = object-hash(color.red); let (grn-id, grn-state) = object-hash(color.green); let (blu-id, blu-state) = object-hash(color.blue); let (merge-id, merge-state) = merge-hash-codes(red-id, red-state, grn-id, grn-state, ordered: #t); merge-hash-codes(merge-id, merge-state, blu-id, blu-state, ordered: #t); end; local method color-test(o1 :: <color>, o2 :: <color>) o1.red = o2.red & o1.green = o2.green & o1.blue = o2.blue; end; values(color-test, color-hash) end method table-protocol; define variable color-table = make(<color-table>); define method make(class == <color>, #key red, green, blue) let prototype = next-method(); element(color-table, prototype, default: #f) | (color-table[prototype] := prototype); end method make;
250
Here is an example of such a program, followed by the equivalent C++: Mix-in example in Dylan.
define class <window> (<object>) slot width :: <integer>; slot height :: <integer>; end class <window>; define class <border-window> (<window>) slot border-width :: <integer>; end class <border-window>; define method width(window :: <border-window>) next-method() - 2 * window.border-width; end method width; define method height(window :: <border-window>) next-method() - 2 * window.border-width; end method height; define class <label-window> (<window>) slot label-height :: <integer>; slot label-text :: <string>; end class <label-window>; define method height(window :: <label-window>) next-method() - window.label-height; end method height; define class <border-label-window> (<border-window>, <label-window>, <window>) end class <border-label-window>;
The example is a greatly simplied sketch of a computer-display windowing system, where a window may have a border (outline decoration), or a title (such as the title bar of a window), or both. (We omit any further detail, such as scroll bars.) One chore in such a system is to compute the available display area of a window from that windows overall size and from the sizes of the windows components. Note that calling height on an instance of <border-label-window> will automatically perform the actions appropriate for a window with a border and a label. First, the method for <border-window> will be called, subtracting out the border width; when it calls next-method, to get the underlying window width, the method for <label-window> will be called, subtracting out the label height; nally, when it calls next-method, the method for getting the value of the height slot in the underlying window will be called. This example is a classic one of the mix-in style the full functionality of the <border-label-window> class is the result of the combination of the individual pieces of <border-window> and <label-window> functionality. C++ equivalent of the mix-in example.
class Window { private: int _width; int _height; public: virtual int width() { return _width; } virtual int height() { return _height; } }; class BorderWindow : public virtual Window {
251
private: int _border_width; public: virtual int border_width() { return _border_width; } virtual int width(); virtual int height(); }; int BorderWindow::width() { return Window::width() - 2 * border_width(); } int BorderWindow::height() { return Window::height() - 2 * border_width(); } class LabelWindow : public virtual Window { private: int _label_height; char *_label_text; public: virtual int label_height() { return _label_height; } virtual char* label_text() { return _label_text; } virtual int height(); }; int LabelWindow::height() { return Window::height() - label_height(); } class BorderLabelWindow : public virtual BorderWindow, public virtual LabelWindow, public virtual Window { public: virtual int height(); }; // Have to generate "combined" method by hand in C++ int BorderLabelWindow::height() { return Window::height() - 2 * border_width() - label_height(); }
It may be helpful for C++ programmers to consider that: Dylan base classes are always virtual. In Dylan, data members are accessed through virtual functions, so it is always possible to override access to a data member in a derived class, and to modify the returned value (or, by overriding the setter, to modify the value to be stored). Dylans next-method allows you to use automatic method combination when you are programming in a mix-in style. Note that the C++ equivalent of the mix-in example is incomplete. It is intended only as a guide to how you can think of Dylan classes. In particular, we have not modeled the slot setter virtual functions that Dylan classes dene automatically, and we have not gone into how instances of the classes are constructed. In Dylan, we would simply give init-keywords for each of the slots, and the automatically generated constructor would ll them in for any of the derived classes. In contrast, constructors for virtual base classes are a particularly difcult aspect of C++: They make
252
it hard to model what is done in Dylan accurately. In general, the mix-in style of programming is more difcult to do in C++, because that languages support for it is quite limited. Note also that the C++ code is provided only as a model of Dylan execution, so that you can understand the semantics of Dylan classes in C++ terms. Good Dylan compilers use library compilation, type inferencing, and partial evaluation to optimize out the overhead normally associated with virtual classes and virtual functions, while preserving the dynamic execution semantics.
253
254
CHAPTER
TEN
GLOSSARY
abstract class A class that cannot have direct instances. To dene an abstract class, you provide the abstract class adjective in the define class form. All superclasses of an abstract class must also be abstract. allocation The allocation of a slot determines where the storage for the slots value is allocated, and determines which instances share the value of the slot. There are four kinds of allocation: instance, class, each-subclass, and virtual. ambiguous methods Methods that cannot be ordered as more specic or less specic than one another, in the method dispatch. assignment The act of setting the value of an existing variable or slot, or of setting an element of a collection. The assignment operator is :=. binding An association between a name and an object. For example, there is a binding that associates the name of a constant and the object that is the value of the constant. The names of functions, module variables, and local variables are also bindings. body A region of program code that delimits the scope of all local variables declared inside it. Bodies can be nested. An body is begun implicitly with define method, and is ended by the corresponding end. You can dene a body explicitly by using begin to start it and end to nish it. A local variable has scope extending from its declaration to the end of the smallest body that surrounds it. built-in class A class provided by Dylan, such as <object>, <integer>, or <string>. class A denition of a type of other objects, which are called its instances. A class denes the slots of its instances. Dylan provides built-in classes, and users can dene new classes. When you dene a class, you specify its name, its direct superclasses, and its slots. class precedence list For a particular class, a list of the class and all its superclasses, ordered from most specic (the class itself) to least specic (the <object> class). closure A method that closes over some local variables. The closure can access the local variables which existed when the closure was created. The ability to dynamically create and return closures that can access lexical state is one of the important dynamic aspects of Dylan. collection A kind of container that can hold zero or more objects. Dylan provides the usual kinds of collections, including arrays, vectors, strings, singly linked lists, queues, hash tables, and so on. In Dylan, a collection is an instance of a class. For example, the <array> class represents arrays, and the <vector> class represents vectors. concrete class A class that can have direct instances. By default, a class is concrete. condition An instance (direct or indirect) of the <condition> class, that represents a problem or unusual situation encountered during program execution.
255
constant An unchanging binding whose scope is its module. You dene a constant explicitly with define constant, and implicitly with define class, define generic, define macro, or possibly define method. You must initialize the value of a constant, and you cannot assign another value to a constant during the execution of a Dylan program. (Also called module constant.) constituent A denition, a local declaration, or an expression. constructor A function that creates an instance. A constructor provides a shorthand means for calling make. For example, you can call the constructor function vector to create a vector, and to initialize that vector with data. contract An agreement between a generic function and its methods. The generic function denes the terms of the contract, and the methods must obey the contract; particularly, the methods parameters and value declarations must be congruent with the generic functions parameters and value declarations. denition A declaration of a piece of program structure, such as a library, module, class, generic function, or method. A denition usually establishes a module variable or constant. Denitions include define variable, define class, and define method. development environment A collection of tools for Dylan programmers that can include an editor custom-tailored for Dylan code, a browser, a compiler, a debugger, and a listener that enables you to enter expressions and to see their values. The features of any development environment are dened by the implementation, rather than by Dylan itself. direct instance An object is a direct instance of class A if the objects class is class A. You can use object-class to nd out the class of which an object is a direct instance. direct subclass A class is the direct subclass of all its direct superclasses. Direct means there is no class intervening between the class and its subclass in the inheritance graph. direct superclass The direct superclasses of a class appear in the define class form for that class. Direct means that there is no class intervening between the class and its superclass in the inheritance graph. dylan library A library that contains modules that contain the elements of the core Dylan language. dylan module A module that contains the elements of the core Dylan language. dylan-user module The special bootstrapping module in which you dene the modules and libraries that make up your program. exception An unexpected event that occurs during program execution. expression A piece of code that, when executed, can return (zero or more) values and can have side effects. Expressions include (among others) literals, references to variables or constants, function calls, and statements (such as if, while, and case). #f The canonical false value. This object is the only object that represents false in Dylan. general instance A member of a class. An object is a general instance of a class if it is either a direct or an indirect instance of that class. The term instance is equivalent to the term general instance. generic function A kind of function. A generic function denes an interface, and contains methods that implement that generic function. When a generic function is called, it chooses the method to call based on the types of its required arguments. getter A method that retrieves the current value of a slot in an object. Each slot in a class automatically has a getter dened for it. The getters name is the same as the name of the slot. handler A function that can potentially resolve an exceptional situation. implicit generic function A generic function created by Dylan if a method is dened by define method or (for a slot getter or setter) by define class and if no generic function of the same name exists. An implicit generic function has the most general parameter and result types that are compatible with the method.
256
indirect instance An object is an indirect instance of class A if the objects class has class A as a superclass. inx function A function whose calling syntax has the function appearing between the arguments. The arithmetic functions +, -, *, /, <, >, and so on are inx functions, as is the assignment operator, :=. An example of the calling syntax is: 3 + 2. information hiding A principle of minimizing the information that is passed among components in a system; it reduces the interdependencies of components. inheritance The ability to arrange for classes that are logically related to one another to share the behaviors and data attributes that they have in common. Each class inherits from one or more other classes, called its superclasses. If no other class is an appropriate superclass, the class inherits from the class <object>. init expression A technique for initializing slots. An init expression provides an expression that yields a default value. Every time that an instance is made and the slot needs a default value, this expression is evaluated, and its value is used as the default. The slot receives its default initial value when no init keyword is dened, or when the caller does not supply the init-keyword argument to make. init function A function of zero arguments that is to be called to return a default initial value for the slot. The function is called every time that an instance is created if no init keyword is dened, or if the caller does not supply the init keyword argument to make. To dene an init function for a slot, use the init-function: slot option in the class denition. init keyword A keyword that can be given to make to provide an initial value for a slot. To dene an init keyword for a slot, you use the init-keyword: or required-init-keyword: slot option in the class denition. init value A default initial value for a slot, obtained by evaluating an expression once, before the rst instance of the class is made. To dene an init value for a slot, use the init-value: slot option in the class denition. initialize To provide an initial value for something that you are creating, such as a slot or a variable. initialize method A method for the initialize generic function. The purpose of initialize methods is to initialize an instance before that instance is returned by make. instance A member of a class. An object is an instance of a class if it is either a direct or an indirect instance of that class. The term instance is equivalent to the term general instance. instantiable class A class that can be used as the rst argument to make. All concrete classes are instantiable. You can make an abstract class be instantiable by dening a make method for the class; the make method must return an instance of a concrete subclass of the abstract class. interchange format A format that all Dylan implementations accept for publishing and exchanging source code by means of les. In this format, each le contains a single source record. The le must have a header at the front, consisting of pairs of keywords and values. One required keyword is module:; its value is the name of the module in which the source record of the le resides. keyword A symbol name followed by a colon, such as total-seconds:. keyword argument An optional argument to a function consisting of a keyword followed by that keywords value. You can give keyword arguments in any order. Keyword arguments can be useful for functions that take many arguments when you call the function, you do not need to remember the order of the arguments. Keyword parameters enable a method to accept optional arguments that are keyed to a name. Keyword parameters appear after #key in the parameter list. library A Dylan library denes a software component, which is a separately compilable unit that can be either a stand-alone program or a component (library) of a larger program. A library contains modules. library-interchange denition (LID) le A le that enumerates all the les that make up a library. Most Dylan implementations support LID les, but these les are not required to by the core language. limited type A type that is a more restricted version of its base type. For example, a limited-integer type is based on <integer>, but has a given minimum or maximum value. Another example of a limited type is a limited-
257
collection type, which is a collection type that species the type of elements, and/or the size of the collection. Limited types are created via limited. listener A tool that enables you to enter Dylan expressions, executes the expressions, and displays any values and output produced by them. literal constant An object whose contents are known completely at compile time. local declaration A declaration that establishes a local variable, local method, or local condition handler. Local declarations include let, local, and let handler. local variable A binding whose scope extends from its denition to the end of the smallest body that surrounds it. You establish and use local variables within a body. Once the program exits the body, the local variables are no longer dened, and an attempt to access them is an error. macro A word or phrase that stands for another phrase (usually longer, but built of simpler components). Macros can be used for abbreviation, abstraction, simplication, or structuring. The primary use of macros in programming is to extend or adapt the language to allow a more concise or readable solution for a particular problem domain. method A kind of function that can belong to a generic function. Although methods are independent of classes, they operate on instances of classes. A method states the kinds of objects that it handles by the types of its required arguments. module A unit that contains a portion of the denitions of a library. Each module species an independent namespace for Dylan constants and variables, and controls the visibility of the names within a module from outside the module. You can use modules both to do information hiding and to prevent name clashes between constants and variables. module constant See constant. module variable A binding whose scope is its module. A module variable is much like a global variable in other languages. You dene a module variable with define variable. When you dene a module variable, you must initialize it (that is, provide an initial value for it). If a module variable is not exported from the module that denes it, then it is accessible only within the module. If the module variable is exported by the module that denes it, and is imported or used by another module, then it is accessible within that other module as well. multiple inheritance Inheritance of a class from more than one direct superclass. <object> class The class from which all classes inherit, either directly or indirectly. object An individual datum. Also called an instance. parameter list A list of specications for the arguments to a function. A parameter list can specify required and optional arguments. The optional arguments can be keyword arguments, each of which is passed to the function as a keyword followed by a value. Each parameter has a name, which is bound to the corresponding argument within the functions body when the function is called. Required parameters and a methods keyword parameters can include type constraints. The parameter lists of a generic function and all its methods must be congruent. parameter specializer The type of a required parameter of a method. predicate A function that returns true or false. False is always represented as #f. True is represented by the canonical true value, #t, and by any value other than #f. protocol The interface denition of a software component. The purpose of establishing protocols is to dene a uniform interface that clients can use, even if the implementation of a component is enhanced or modied. recursion A technique in which a function calls itself. required parameter A parameter corresponding to an argument that must be provided in the call to the function. Required parameters appear before any rest or keyword parameters in a parameter list. Required parameters are ordered, and the required arguments must be given in the same order.
258
rest parameter Parameters that enable a method to accept any number of optional arguments. Any arguments provided in the call after the required arguments are collected in a sequence, which is the value of the rest parameter. A rest parameter, if one exists, appears after #rest in the parameter list. restart A special condition that represents an opportunity to recover from an exception. restart handler A function used to implement the particular recovery action for a restart condition. root The starting point of Dylan class inheritance the class <object>, from which all Dylan classes inherit, either directly or indirectly. setter A method that stores a value in a slot. By default, each slot in a class has a setter dened for it automatically. signature The parameter list and the values declaration of a function. singleton type A type whose only member is one particular instance. Singleton types are created via singleton. single inheritance Inheritance in a class that has only one direct superclass. slot A unit of data associated with an instance. A slot is like a structure member or a eld in other languages. Information about a slot is specied in the denition of the instances class. The location of storage for the slot is determined by the slots allocation. A program retrieves the value of a slot by calling that slots getter generic function, and, unless the slot is constant, it sets the value by calling the slots setter generic function. slot option An option that species a characteristic of a slot, such as the default initial value or the init keyword. Slot options appear in the define class form. source record A unit that organizes a portion of the Dylan source code for a program. Different Dylan implementations divide code into source records differently, and store the source records differently. For example, an implementation might store source records in a database. Many implementations store source records in les, and typically each le contains one source record. subclass The subclasses of a class include the class itself, and all classes that inherit from the class (all the classs direct subclasses, and all their direct subclasses, and so on). subtype The subtypes of a type include the type itself, and all types that inherit from the type, directly or indirectly. superclass The superclasses of a class include all that classs direct superclasses, and all their direct superclasses, and so on, all the way to the root of class inheritance, which is the <object> class. You can use all-superclasses to nd all the superclasses of a class. supertype The supertypes of a type include all the types from which the type inherits, directly or indirectly. symbol An instance of the <symbol> type. Symbols are much like strings. There are two reasons to use symbols in certain cases where you might consider strings. First, symbol comparison is not case sensitive. Second, comparison of two symbols is much faster than is comparison of two strings, because symbols are compared by identity, and strings are usually compared element by element., There are two equivalent syntaxes for referring to symbols: north: is an example of the keyword syntax, whereas #"north" is an example of the hash syntax. #t The canonical value of true. Note that any value other than #f is considered a value of true. type An object that describes the structure and behavior of its members. All classes are types, but not all types are classes. You can dene new nonclass types with limited, singleton, and type-union. type constraint A type associated with a binding or slot that ensures that the value of that binding or slot can hold only objects of that type. union type A type whose members include all the members of one or more base types. Union types are created via type-union. user-dened class A class dened by a Dylan user, and not provided by Dylan itself.
259
value declaration A list of the values returned by a function, and of the types of the values. The name of a return value is used purely for documentation purposes. When you provide a value declaration for a function, Dylan signals an error if the function tries to return a value of the wrong type. The compiler can check receivers of the results of the method for correct type, and can usually produce more efcient code. The value declarations of a generic function and all that functions methods must be congruent. virtual slot A slot that does not occupy storage; instead, its value is computed. When you dene a virtual slot, you need to dene a getter method to return the value of the virtual slot, and you can optionally dene a setter method to set the value of the virtual slot. genindex search
260
INDEX
Symbols
#f, 256 #t, 259 <object> class, 258
E
even?, 16 exception, 256 expression, 256
A
abstract class, 255 allocation, 255 ambiguous methods, 255 assignment, 255 auxiliary macros, 238
F
function macro, 232
G
general instance, 256 generic function, 256 getter, 256
B
binding, 255 body, 255 built-in class, 255
H
handler, 256
C
class, 255 naming conventions, 18 class precedence list, 255 closure, 255 collection, 255 concrete class, 255 condition, 255 constant, 256 module constant, 18 naming conventions, 19 constituent, 256 constructor, 256 contract, 256 curry, 99, 121
I
implicit generic function, 256 indirect instance, 257 inx function, 257 information hiding, 257 inheritance, 257 init expression, 257 init function, 257 init keyword, 257 init value, 257 initialize, 257 initialize method, 257 instance, 257 instantiable class, 257 interchange format, 257
D
dening macro, 236 denition, 256 development environment, 256 direct instance, 256 direct subclass, 256 direct superclass, 256 dylan library, 256
K
keyword, 257 keyword argument, 257
L
library, 257 261
library-interchange denition (LID) le, 257 limited type, 257 listener, 258 literal constant, 258 local declaration, 258 local variable, 258
R
recursion, 258 required parameter, 258 rest parameter, 259 restart, 259 restart handler, 259 root, 259
M
macro, 231, 258 auxiliary macros, 238 constraints, 235, 240 dening macro, 236 delay of evaluation of arguments, 232, 233 evaluation in, 234 function macro, 232 hygiene, 233, 237 pattern variables, 232, 240 patterns, 231 rules, 231 statement macro, 235 templates, 231 make, 32 methods, 250 map, 99 method, 258 module, 258 module constant, 18, 258 module variable, 258 introduction, 16 multiple inheritance, 258
S
setter, 259 signature, 259 single inheritance, 259 singleton type, 259 slot, 259 virtual, 87 slot allocation class, 87 each-subclass, 87 instance, 87 virtual, 87 slot option, 259 source record, 259 statement macro, 235 subclass, 259 subtype, 259 superclass, 259 supertype, 259 symbol, 259
T
templates, 231 type, 259 naming conventions, 75 non-class types, 75 type constraint, 17, 259 of slots, 32 performance implications, 203
N
naming conventions class, 18 constant, 19 predicate, 16 type, 75 variable, 16 non-class types, 75
U
union type, 259 user-dened class, 259
O
object, 258
V
value declaration, 260 variable naming conventions, 16 type constraints, 17 vectors creation and access to elements, 92 virtual slot, 87, 260 virtual slot allocation, 87
P
parameter list, 258 parameter specializer, 258 pattern variables, 232, 240 patterns, 231 performance type constraints, 203 predicate, 258 naming conventions, 16 protocol, 258
W
while, 96 Index
262
Z
zero?, 16
Index
263