There's a easier and more powerful way to solve that kind of problem.
It is to write a DSL (ie. macros) in Lisp, to generate correct code in
the first place.
Of course, you will object how to be sure the macros are correct?
I'll object how to be sure your type expressions are correct?
The compiler checks them. The type and object system ends up being a
DSL, but one whose syntax and type-semantics can be checked by
automation. Your DSL will just crash with some run-time error if you
goofed.
The major benefit is with later, incremental changes rather than
during system design though. Making the incremental change to the
static-type system is like plugging something in where, if you get the
plug backwards, it won't fit. Making the incremental change to the
dynamic-type system is like plugging something in where, if you get
the plug backwards, it seems fine until you flip the on switch, and
then you have inconvenienced the local fire brigade.
There's a tradeoff, to be sure. Dynamic seems to beat static for small
jobs and certain kinds of tinkering, and for things that are used
interactively by a hacker that can fix or reprogram them on the fly.
For stand-alone deployments of large systems to non-developer users,
on the other hand, static seems to win out.
Static does mean spending some time figuring out and then coding the
type system. However, the time spent *figuring out* the type system
needs to be spent no matter what, or the durn thing won't work. The
time spent *coding* the type system does two things:
1. It documents the types of things in the code. If the code will
ever have to be maintained by someone unfamiliar with it,
including perhaps your own future self after a hiatus, this will
need to be done anyway or the code won't be maintainable.
2. It lets the compiler catch type errors quickly, and locate them
quickly. With dynamic typing, a type error will produce a
run-time error that may appear far from the actual location of
the bug, for instance of the wrong sort of object is added to
a collection and much later removed, then an operation invoked
on it that it doesn't support. I'm not sure what you'd get in
lisp, probably a logic error rather than an exception; Smalltalk
would produce the dreaded "doesNotUnderstand" box and Java a
ClassCastException or NoSuchMethodError (Object taken from
raw List and either cast or operated on reflectively).
Java with a List<Foo> is another story: as soon as you write a
line of code that tries to put a non-Foo into the list, it
gets a red wavy underline in your IDE!
This gets into the flip-side of the tradeoff. Besides developing the
type system and documenting the types of values and variables in the
code, which probably needed to be done in some manner anyway, you've
traded off a bit more coding time for a lot less debugging time. Some
errors you will discover within *seconds* of making them that would
have required *hours* to hunt down otherwise. That's savings you can
take to the bank. Literally, if you're a manager paying a bunch of
coders' salaries, and their productivity has just shot up through the
roof.
The system size is probably key, and whether the end-user can be
expected to interactively debug and tinker with it. For small systems,
the code base is not large enough for hunting down the type error to
take hours, and dynamic languages seem to better support on-the-fly
modification for various reasons. Glue scripts are another case in
point.
Furthermore, it's worth noting that most "statically typed" languages
give you the option to take or leave compiler validation of your type
logic, while most dynamically typed languages are exactly that -- no
such option, you can't have it even if you want it.
C lets you dodge the type system completely with arbitrary casts and
the "union" construct. So does C++, though C++ adds a dynamic_cast
operator that acts like Java's cast (i.e. is a type-checking cast).
Java, as noted, lets you use nothing but Object and reflection, or
lots of casting and untyped collections, if you want to (most Java
programmers don't, though -- I wonder why?). All of them let you
eschew object orientation and just have structs/classes with public
fields and external functions to manipulate these if you want to, for
that matter. Heck, you can defeat the type system in Java almost
entirely by using arrays of Objects, eschewing named fields and the
primitive types like int and double.
Nobody does this though.
And unless your type system is Turing Complete, there are necessarily
specifications you cannot write.
The type system doesn't need to be Turing-complete unless it needs to
be genuinely dynamic, changing at run-time, or else essentially
embodying the entire semantics of the code.
Type systems normally embody the *static* aspects of the code only --
what sorts of things go here, what this thing can do rather than what
specific thing is here, what this thing is currently doing.
When a complex relationship exists, like "this should be an integer
unless that's null, and then it should be a Rocket instead" are better
expressed with an encapsulated object, with an integer field, a Rocket
field, and some other field, and the other field determining which of
the first two are in use. Callers to the object needn't concern
themselves with this internal matter, and the preservation of the
required invariants is the responsibility of the small amount of code
in one single class. If the invariant is ever violated, you can
immediately narrow down the location of at least one bug to that small
bit of the codebase.
If a similar invariant is ever violated in a dynamic system that
accesses those fields from all over and has complex, Turing-complete
type-system rules of some sort, you'll have to hunt for it all over
the place, by contrast. Any code that touches those fields is suspect.
Indeed, since the object with these fields could have snuck into
someplace it didn't belong, without a peep from the compiler, any code
at all is suspect, since code meant to store an integer in a hash
field in something else might have accidentally overwritten your
Rocket after being passed the wrong type of object. And of course the
code that represents the Turing-complete type-system rule might
contain the error. Maybe the fields didn't get monkeyed with at all,
and the run-time error claiming they did was itself issued in error.
And when your type system is Turing Complete, there are necessarily
bugs written in it, and a lot of time spend on it, because of the
syntactic complexity (eg. C++ templates!).
Upfront cost, downstream savings (if done right).
Lisp macros are just a much simplier and much more powerful to archive
the same results much faster.
What results? Compile-time type safety? I doubt it, unless you're
writing a C or Java compiler in Lisp.
Maybe you mean to suggest that the type-checking be done dynamically,
at run-time, with type-checking code everywhere when things are added
to/retrieved from collections, passed as parameters, and everything.
Java does this to some extent, when there's a need to cast something
or when something's taken out of a generic collection. However, in
many circumstances like parameter-passing the type-checking has no
runtime overhead because the compiler was able to verify everything
before the program ran.
This is another area where dynamic tends to lose to static:
performance. In the absence of dynamic type checking, the system is
probably somewhat buggy and unstable, which costs in downtime. In the
presence of dynamic type checking, the system may be more robust,
because bugs were easier to find and fix during testing, but areas not
covered by testing will be dodgy and the dynamic type checking takes
its toll on speed. In the presence of static type checking, 100% of
the code is free of type errors according to the compiler, before it's
even run, so areas not covered by testing still won't contain type
errors (though they may contain logic errors). The smaller amount of
dynamic type checking takes much less of a toll on performance. Last,
but certainly not least, because your compiler actually groks the type
system it may be able to make all kinds of clever optimizations that
would not be safe to make on dynamically-typed code. The speed boost
from this alone could be the deciding factor for CPU-bound tasks, or
for systems with strict real-time requirements (which, in practice,
are almost always coded in C).