Dylan Type Declarations and Rapid Prototyping
Dylan’s type declarations are
optional. Combined with a flexible, polymorphic type system, this can be a boon
for real-world development cycles that include rapid prototyping and iterative
refinement.
In Dylan, type declarations are optional.
For example, we can write a
factorial
function like so:
define
method factorial
(x)
if (x <
1)
1
else
x
* factorial( x - 1
)
end
end;
Notice
that the type of
x is not
declared, and neither is the return type of the function. If you’re used
to languages like C where type declarations are required everywhere, you may be
wondering what this means. Is this type unsafe? Will this function crash if we
pass in, say, a string? The answer in both cases is no, but it raises some
interesting questions about what it means to be
“type-safe.”
Dylan is
a type-safe language. By that I mean that if you call this function with any
value that cannot be passed to the functions
<,
*, or
-, an
error will be signaled by one of those functions when
factorial
calls them. Typically, this means that you can call
factorial
with any type of number, including any size of integer or real. In C, this would
be a problem, because
factorial
could only accept an integer or a floating point number. In C it would be an
error to call
factorial
with a string or a struct regardless of whether
<,
*, and
- are
defined for those types.
In
contrast, you could call the Dylan version with any type for which you define
methods on
<,
*, and
-. You
could arrange for
factorial
to work on strings. What the result would mean, exactly, is another topic, but
the function would run without signaling a type
error.
What this means is that
factorial
has an implicit type declaration: “Any value for which
<,
*, and
- are
defined.” (To be exact, any value for which
< can
accept that value and the number
1,
* can
accept that value and the result of calling
factorial,
and - can
accept that value and
1 —
or for which <
1 returns true, in which case the other
functions will never be
called.)
Now, in practice we might
decide early on that we only want to define
factorial
for numbers. That’s easy enough to do. We just change the definition of
factorial
to:
define method
factorial (x :: <number>)
…
This means two things: we
can now only call
factorial
with a number regardless of whether the functions it calls could handle more
types, and since we know that those functions handle all types of numbers this
function will never signal a type
error.
Finally, let’s say we
later decide that we really only want to define
factorial
for integers, in which case we can easily change our code
to:
define method
factorial (x :: <integer>)
…
Now, for a well-defined
math function like
factorial
this may not seem very useful, but consider real-world programming. Typically, a
program is not completely predefined down to every last statement. If it were,
you’d be done; the definition would be the program. In real-world
programming, we get a rough idea of what we want a program to do, we write some
of it, and use what we learn from trying to code it to refine our definition.
This process repeats itself, getting the code and our understanding of what we
want it to do closer and closer to a finished product. At some point, this
refinement segues into debugging and the focus on creating code diminishes
(until somebody decides to change the requirements for our program two weeks
before shipping).
This process
often starts with prototyping, where even the initial definition is a bit fuzzy
(even if we don’t realize it). Of course, we all know we’re supposed
to throw away the prototype once we’ve gotten a better handle on the
requirements and the program definition, but the reality is that code that works
tends to live on (sometimes longer than it should). So, it’s important
that programming languages smoothly integrate prototyping and “real”
programming using iterative
refinement.
One way Dylan supports
this development process is by allowing you to quickly write code that works,
with either more general types or no type declarations at all, then later add
declarations or make them more specific as the requirements and the details of
the implementation become better understood. It means that you can specify
programs with whatever level of detail you have at any given moment to rapidly
“get the code working,” then refine that code, transitioning it
smoothly into something you can ship with less disruption to the code along the
way.
Posted: Fri - February 20, 2004 at 01:06 AM