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          


©