I am sure that for many "elegance" problems of static languages, static type checking itself isn't to blame, but the lack of expressiveness of the static type system implemented in the language and the limited capabilities of the compiler. If this is done "righter" (like in Haskell for example), then suddenly the programs turn out to be terse, elegant .. and safer that their dynamic counterpart.
Here's an illustration (C++ specific, sorry): C++ is so powerful, that it implements a metalanguage with it's template class system. But still, a very simple function is hard to declare:
template<class X,class Y>
? max(X x, Y y)
There is an astounding amount of possible solutions, like ?=boost::variant<X,Y>
or computing ?=is_convertible(X,Y)?(X:is_convertible(Y,X):Y:error)
, none of them really satisfiying.
But now imagine a preprocessor, that could transform an input program into it's equivalent continuation passing style form, where each continuation is a callable object which accepts all possible argument types. A CPS version of max would look like this:
template<class X, class Y, class C>
void cps_max(X x, Y y, C cont) // cont is a object which can be called with X or Y
{
if (x>y) cont(x); else cont(y);
}
The problem is gone, max calls a continuation which accepts X or Y. So, there is a solution for max with static type checking, but we can't express max in it's non-CPS form, untransform(cps_max)
is undefined, so to speak. So,we have some argument that max
can be done right, but we don't have the means to do so. This is lack of expressiveness.
Update for 2501:
Assume there are some unrelated types X and Y and there is a bool operator<(X,Y)
. What shouldmax(X,Y)
return? Let us further assume, that X and Y both have a member function foo();
. How could we make it possible to write:
void f(X x, Y y) {
max(X,Y).foo();
}
returning either X or Y and invoking foo() on the result is no problem for a dynamic language, but close to impossible for most static languages. However, we can have the intended functionality by rewriting f() to use cps_max:
struct call_foo { template<class T> void operator(const T &t) const { t.foo(); } };
void f(X x, Y y) {
cps_max(x,y,call_foo());
}
So this can't be a problem for static type checking, but it looks very ugly and does not scale well beyond simple examples. So what is missing from this static language that we can not provide a static and readable solution.