Effective Dylan: 50 Specific Ways Dylan is Easier to Use Than C++
A point-by-point comparison between Dylan
and the fifty items described in Scott Meyers' Effective C++.
I’ve been meaning to do this for a
while. Here I go. The following is a brief† explanation of how Dylan
compares against all fifty items listed in Scott Meyers’
Effective C++: 50
Specific Ways to Improve Your Programs and
Designs††. Much as
I like this book and find it useful for programming in C++, the fact is that a
lot of what’s in it (and other books on C++) is about the unnecessarily
hard parts of C++. Dylan generally provides a simpler and more productive (and
enjoyable!) approach to
programming.†I wrote that
before writing the bulk of this. Since there are, after all,
fifty
points, the overall result isn’t very short. However, my response to each
item is relatively brief and may leave out a number of details that would be
worth expanding on in the
future.††Note that I
used the first edition of Scott’s book, although that link above points at
the second edition on Amazon. At some point I’ll go through the second
edition and update my comments if
necessary.Shifting from C
to C++This section is very
specific to C vs. C++, so there isn’t a direct comparison in Dylan, but
I’ll add some comments
anyway.1.
Use const and inline instead of
#define. Dylan has no preprocessor or
#define. It has constants and inline
functions.2.
Prefer iostream.h to
stdio.h. Dylan has stream-based I/O
libraries.3.
Use new and delete instead of malloc and
free. Dylan has automatic memory
management. You do not need to call functions to allocate and deallocate
memory.4.
Prefer C++-style
comments. Dylan supports both styles of
C++ comments (/* */ and //), and common Dylan programming style agrees with
Meyers.Memory
ManagementMost of the items
in this section are non-issues in Dylan, which has automatic memory management.
There are no new and delete to use
incorrectly.5.
Use the same form in corresponding calls to
new and delete. An impossible error to
make in Dylan. If you create a collection object, every object in it will be
automatically deallocated as appropriate when the collection is
deallocated.6.
Call delete on pointer members in
destructors. Another detail that’s
impossible to overlook in Dylan. All objects are properly deallocated when no
longer referenced.7.
Check the return value of
new. Dylan’s equivalent to new,
“make,” signals an exception if object allocation or initialization
fails. It is not possible to unintentionally overlook a
failure.8.
Adhere to convention when writing
new. There is no
new.9.
Avoid hiding the global new.
There is no global
new.10.
Write delete if you write
new. There is no…rule number
six.Constructors,
Destructors, and Assignment
OperatorsThese issues are
dramatically simpler in Dylan. Automatic memory management eliminates the need
to define constructors and destructors when the only resource being managed is
memory. Initialization is much simpler in Dylan and in most cases can be defined
in the class definition without a separate initialization function. There is no
assignment operator function, and, in contrast to C++, Dylan doesn’t copy
objects implicitly, and explicit copying is
rare.11.
Define a copy constructor and an assignment
operator for classes with dynamically allocated
memory. Although you can define the
equivalent of copy constructors when desired in Dylan, automatic memory
management makes it unnecessary to worry about writing copying code merely to
properly manage memory. There is no copying assignment operator in Dylan, and no
implicit copying of objects, so it is rare that you need to write copying code;
you do not need to do so simply to satisfy language semantics as in C++, you
only need to do so when you wish to support explicit
copying.12.
Prefer initialization to assignment in
constructors. This actually applies to
Dylan, although there’s less of a chance a Dylan programmer will make a
mistake here. For most cases, object initialization can be expressed just once,
in the class definition, unlike C++ initialization clauses, which cannot be used
in as many cases and which have to be diligently written for every constructor.
Dylan only requires an explicit initialization function for special cases, e.g.,
when multiple initial values have
interdependencies.13.
List members in an initialization list in
the order in which they are declared. A
non-issue in Dylan, where initial values are given in the definition of each
member (called “slots” in Dylan), so there is no way to get the
order wrong.14.
Make destructors virtual in base
classes. A non-issue in Dylan, where all
functions are implicitly “virtual” (besides, Dylan does not have
destructors in the C++ sense.) Note that this does
not
mean all function calls incur dispatching overhead, a cost many C++ programmers
are very conscious of. If a given call site doesn’t require dynamic
dispatch, it automatically becomes a simple function call, just as with
non-virtual C++ member functions, and may even be inlined as appropriate. There
is no need (or means) to explicitly declare “virtual” and
“non-virtual” functions, and no class definition changes are ever
necessary if program requirements later
change.15.
Have operator= return a reference to
*this. There is no assignment operator in
Dylan.16.
Assign to all data members in
operator=. There is still no assignment
operator in Dylan.17.
Check for assignment to self in
operator=. Well, there was one, but the
cat’s eaten it.(Someone
asked me for further clarification: There is an “assignment
operator” in Dylan, of course. It looks just like Pascal’s
“:=”. However, it is not a function. It is not equivalent to
C++’s operator=, which performs copying, and there is no need to implement
one for your classes as in C++ simply to avoid problems with C++’s
pervasive implicit
copying.)Classes and
Functions: Design and
DeclarationMeyers’ has
some good things to say about class and function design that, broadly speaking,
apply as much to Dylan as they do to any other language. However, a lot of these
items are rendered simpler—or in some cases, trivial—to deal with in
Dylan.18.
Strive for class interfaces that are
complete and minimal. Okay, Scott, will
do. In fact, in Dylan, class definitions are usually much simpler than in C++,
only including definitions of slots (“data members”) and
(implicitly) their accessors—an approach Scott recommends
for C++, but which is made more difficult to adhere to than in Dylan
because of the limitations of non-member
functions.19.
Differentiate among member functions,
global functions, and friend functions.
Dylan has a simpler, more orthogonal programming model in which there is
essentially only one kind of function (called a “generic function”),
which is polymorphic, similar to a C++ virtual member function. Generic
functions do not “belong” to classes, and there is no real
distinction between “member” and “non-member”, or
“friend” functions, making it simpler to evolve programs with fewer
and more localized source changes. You use explicit modules (aka
“namespaces”) to control access and encapsulation instead of
implicit class namespaces and public, private, protected, and friend
declarations.20.
Avoid data members in the public
interface. In Dylan, accessor functions
are automatically defined for all slots. In fact, there is no way to explicitly
access a slot “directly” as in C++. All slot access is via accessor
functions (calls to which are often optimized away in practice). In C++, you
have to explicitly write your own accessors, complicating the code and
increasing the likelihood that someone will avoid that work and make a data
member public when they shouldn’t, and thereby make the code harder to
maintain and evolve.21.
Use const whenever
possible. Dylan doesn’t have const,
which isn’t as clearly necessary as in C++. One reason for this is that,
just as functions can accept multiple arguments, in Dylan they can also return
multiple values, so you never need to design functions that take
“output” pointer/reference arguments, and so there’s less
need to differentiate them from “input” arguments. Dylan functions
also tend to be functional rather than mutating (ie., they are implicitly
const). Although Dylan explicitly supports imperative programming, most standard
library functions do not alter their arguments, and so const-like declarations
would often be redundant. (This is not to say there would be no value in having
something like
const
in Dylan, but it isn’t as much of a clear win as in
C++.)22.
Pass and return objects by reference
instead of by value. Dylan always uses
reference semantics. There is no direct way to pass by value, although you can
explicitly copy an object then pass the copy
along.23.
Don’t try to return a reference when
you must return an object. See #22. This
simply isn’t an issue in
Dylan.24.
Choose carefully between function
overloading and parameter defaulting.
This applies only indirectly to Dylan. First, it’s important to note that
Dylan does not have function overloading in the same sense as C++. Second,
Meyers recommends overloading when there is no reasonable default value for an
argument, but in Dylan it is always possible to define a default value (using
type-unions, which I won’t go into here), and furthermore, it is possible
to tell whether an optional argument was passed to a Dylan function, obviating
the need for a default value if avoiding one is
desired.25.
Avoid overloading on a pointer and a
numerical type. In Dylan there are no raw
pointers and therefore no null pointer, but more to the point, Dylan is more
type-safe than C++ and there is no way to confuse a zero with a null pointer, or
in fact any number with any value that is not a number. In stark contrast to
C++, Dylan’s false, zero, and null character ('\0') are of distinct types,
and there is no implicit casting between them as in
C++.26.
Guard against potential
ambiguity. There is no implicit casting
and no ambiguity between construction and conversion; you have to explicitly
invoke the desired function. Generic functions do not “belong” to
classes in the same sense that C++ member functions belong to their classes, and
so the potential ambiguity between two inherited member functions with the same
name doesn’t have a direct analogue in Dylan. The closest match is name
collisions in modules (again, aka “namespaces”); similar to C++, you
do need to resolve any collisions that occur during importing, by excluding or
renaming one or more of the offending
names.27.
Explicitly disallow use of implicitly
generated member functions you don’t
want. The example Meyers uses is the
default assignment operator. Dylan has no assignment operator, so that
specifically is a non-issue. More generally, Dylan doesn’t automatically
define any functions on your behalf that you would need to suppress (any
undesirable behaviors are up to you to implement
yourself).28.
Use structs to partition the global
namespace. Here Meyers is suggesting a
short-term workaround for the lack of namespaces in C++. Now that modern C++
compilers support namespaces, this is unnecessary. As mentioned above, Dylan has
namespaces (called
“modules”).Classes
and Functions:
ImplementationSome of these
points discuss design issues of a general nature that tend to apply across
languages, but others highlight where C++ is more complex than one might desire,
where Dylan provides a simpler and more enjoyable programming experience in
contrast. In particular, Dylan has no header files and instead relies upon
whole-program analysis, where the compiler can see all the source code for a
program or library and isn’t constrained by a monolithic,
compilation-order-dependent mechanism like C++’s translation
unit.29.
Avoid returning “handles” to
internal data from const member
functions. As a general point, this
applies to Dylan, and I’d modify this to the more broad, “avoid
exposing implementation information through interfaces.” Note, however,
that Dylan has no raw pointers, and that all slots are implicitly accessed via
functions, so less raw data is exposed by
default.30.
Avoid member functions that return pointers
or references to members less accessible than
themselves. This applies to Dylan in the
same general way that #29 does, though, again, there is no way to return a raw
pointer or reference to a member, making this (at least slightly) less of an
issue.31.
Never return a reference to a local object
or a dereferenced pointer initialized by new within the
function. You can blatantly ignore this
in Dylan, where it’s perfectly safe to return any object, since
you’re always using heap semantics (ie., there is no way to return a
reference a stack object). Note that I said heap
semantics.
Dylan programs are written as though objects are always allocated on the heap,
but objects may be allocated on the stack if there are no external references to
them. Dylan treats the location of an object as an implementation detail, and
allocating on the stack is an optimization. Automatic memory management
eliminates the problems seen in C++ due to incorrect references or omitted
deletion.32.
Use enums for integral class
constants. The opposite is true in Dylan:
Go ahead and use constants, that’s what they’re for. The Dylan
compilation model doesn’t use headers. Instead, the compiler simply looks
at the definition of a constant regardless of where it is in the sources to find
its value. So, Dylan doesn’t have the same scoping and compilation order
problems that C++ does.33.
Use inlining
judiciously. [Curly voice] Why, soitenly.
In fact, Dylan compilers make judicious use of inlining on your behalf in many
cases, and, again, Dylan doesn’t use headers, so you don’t have to
place function bodies in headers to get them inlined. The compiler can inline
any function in your program as appropriate, making inlining easier to manage
with fewer changes to source code. Dylan compilers are also expected to perform
certain kinds of inlining and partial inlining that you might not expect of a
C++ compiler (or that might not be possible due to the language semantics and
compilation model).34.
Minimize compilation dependencies between
files. Of course, reducing dependencies
is a good thing, but again, since there are no headers, the task is made much
simpler in Dylan. No implementation details are explicitly exposed in header
files. Only those dependencies that actually occur in your programs need cause
recompiles, and only the affected functions need to be processed, rather than
recompiling everything in a translation unit that directly or indirectly may
depend upon a header file, as in
C++.Inheritance and
Object-Oriented DesignSome of
these items are general and apply to Dylan, but others are, again, non-issues in
Dylan, or in fact the opposite of common Dylan practice. C++ imposes some design
constraints that Dylan programs are not subject
to.35.
Make sure public inheritance models
“isa.” Dylan doesn’t
have private inheritance, so you can ignore some of what Meyers has to say here,
but the general point about inheritance modeling “isa” still
basically applies to Dylan (and in fact, to all OO
languages).36.
Differentiate between inheritance of
interface and inheritance of
implementation. A good idea in Dylan,
too. Meyers goes on at length on this topic, and the summary is that Dylan is a
bit simpler here because there are no non-virtual member functions, nor pure
virtual member functions. To define an abstract class in Dylan, you do so
explicitly with the adjective “abstract” in the class definition;
there is no need to define a pure virtual member function merely to implicitly
make a class uninstantiable.37.
Never redefine an inherited nonvirtual
function. An impossibility in Dylan, as
all functions use “virtual” semantics. This makes it simpler to
modify and maintain Dylan programs without breaking client
code.38.
Never redefine an inherited default
parameter value. In Dylan it is perfectly
fine to do so, whereas C++ semantics make this problematic. Subclasses can
impose further restrictions and therefore it is desirable to alter default
initialization values, and Dylan semantics make this trivial to
do.39.
Avoid casts down the inheritance
hierarchy. Because it is a dynamic
language, Dylan does not have or need up- or down-casting. Furthermore, it does
not have raw pointers or non-virtual functions, eliminating most of the issues
raised by Meyers here. As he suggests, though, you should avoid relying upon the
exact type of an object and instead provide one or more polymorphic functions
that provide the desired tests for object attributes (in this case, the example
is testing an object’s class, but this rule can be applied more
generally).40.
Model “has-a” or
“is-implemented-in-terms-of” through
layering. Generally applicable to Dylan,
too.41.
Use private inheritance
judiciously. There is no private
inheritance in Dylan, so this is a non-issue. More specifically, Meyers points
out some cases where C++ requires using private inheritance instead of the more
desirable layering approach. In Dylan, these cases simply do not occur, and
layering is always applicable.42.
Differentiate between inheritance and
templates. Dylan directly supports
generic programming without the use of a specialized syntax like C++ templates,
and it is considered good form to make the difference transparent. Most Dylan
programming is generic programming to at least some degree, making it easier to
reuse code. Dylan also supports homogenous and heterogeneous containers. In
fact, unlike in C++, neither of these features requires generating duplicate
code (although this can be done as an optimization, as a form of
inlining).43.
Use multiple inheritance
judiciously. Taking the topic title
literally, it also applies to Dylan. However, MI in C++ has a number of
complications that MI in Dylan simply doesn’t suffer from, making MI use
easier and more common. Mixin inheritance is a common idiom in
Dylan.44.
Say what you mean; understand what
you’re saying. This is sort of a
summary of some of Meyers’ previous points. I won’t bother going
into detail. If you read his book and my comments, it should be straightforward
to see how his points apply to
Dylan.MiscellanyThese
are general points, for the most part, and apply to Dylan, although the details
differ.45.
Know what functions C++ silently writes and
calls. Speaking very,
very
broadly, this is good advice for Dylan, too. However, in stark contrast to C++,
Dylan writes and calls very little code implicitly, and any implicit code does
the right thing (whereas, for example, in C++, if you have any pointer data
members the default destructor will not call delete on them). In fact, Dylan
really only generates accessor functions for slots, and they’re trivial.
Default methods on make() and initialize(), which create and initialize objects,
do the right thing by default. There is no copying assignment operator, copy
constructor, or address-of operators as generated in
C++.46.
Prefer compile-time and link-time errors to
runtime errors. The same is true of
Dylan, although unlike C++ you are guaranteed to get runtime errors (exceptions)
instead of silent failures for things that can’t be checked at compile- or
link-time.47.
Ensure that global objects are initialized
before they’re used. This is
guaranteed in Dylan. All globals (and locals, too) must have an explicit initial
value. There is no way to forget to initialize one. Furthermore, Dylan makes it
easier to specify initial values, and it guarantees that globals with
dependencies will be initialized in the correct
order.48.
Pay attention to compiler
warnings. A good idea no matter what
language you’re using.49.
Plan for coming language
features. Most of Meyers’ points
here are very specific to C++. I’ll just point out that one of
Dylan’s greatest strengths is that it makes it easier to change
things.50.
Read the
ARM. Read the Dylan Reference
Manual. It’s shorter and simpler than the ARM. To be fair, a lot
of the “motivation” discussion in the ARM won’t be found in
the DRM. Instead, look to other sources, such as info-dylan
email and comp.lang.dylan
Usenet archives, as well as Dylan
Programming.
Posted: Sun - April 11, 2004 at 06:41 AM
|
Quick Links
Calendar
| | Sun | Mon | Tue | Wed | Thu | Fri | Sat
|
Categories
Archives
XML/RSS Feed
Blog Roll
Statistics
Total entries in this blog:
Total entries in this category:
Published On: Aug 09, 2007 05:49 AM
|