Type Safety and the Explicit Cast

I think statically typed programming languages are great. Why? Because in just about every dynamically typed language I’ve ever used in earnest—JavaScript, for example—at some point I’ve had to write a lot of code to verify that a value looks like a certain type (one often can’t get a full guarantee) in order to make assertions about the theoretical correctness and consistency of a program. Is that a string? An array? A number? I can’t tell you until I’ve looked. For better or worse, JavaScript will allow absolutely anything (or nothing at all!) as a parameter. Sure, it’s a simple way to implement polymorphic functions without a whole lot of fuss…at first. But most functions I write are intended to work on a set of values that is much, much smaller than the set of values it is not designed to work on.

At a medium-to-high level, this is a non-issue with static typing. I specify a function to operate on values of specific types, and the program won’t even compile if someone tries to pass something different.

Apologists for dynamic languages have pointed to the verbosity and inflexibility of having to specify the type of every single thing, but advances such as type inferencedetermining the type of something based on how it is usedand type-safe genericsfunctions and objects that can operate on arguments of arbitrary types if those types fit general criteriahave made it easier for programmers to convey all of the intended meaning without sacrificing too much flexibility or writing too many more words than necessary.

Another important point to consider is the failure mode of a program that isn’t written correctly. Inconsistencies in the use of values and types tends to be a compile-time error for static languages; they are caught before the program is even run. In a dynamic language like JavaScript, no static types are enforced per se, but using a value as if it is something it’s not can result in a run-time error in the best case[1] and instead often “succeeds”, producing unexpected results [2].

Of course, I don’t like the bits of the static languages that continue to allow type-unsafe behavior. In my opinion, the major violators are:

  • Static casts, such as Java/C# String s = (String)o for some Object o
  • Instance-of as a boolean operator (more of an accomplice than a criminal)
  • Inheritance-based polymorphism used in place of tagged unions
  • Null being an allowed (and often non-disallowable) value for any reference type

Starting with the last one: It really seems strange to me that we automatically allow null to be passed as an object, even if it is an artifact of C (a reference being implemented as a pointer, a null corresponds to a NULL pointer). I think it’s a more reasonable assumption that the object must be valid unless we say it’s optional.

What does this have to do with types? Null degrades the utility of a type if it can’t be disallowed by allowing a value outside the value space to be assigned to a variable of the type. When you pull a value back out, you may need to verify that it isn’t null before continuing. This sounds like that type identification routine I didn’t want to write in JavaScript!

Scala very nearly has it right, offering up the Option monad from the functional programming world, which more or less accepts either a value from an existing type or the solitary None value. However, Scala also still has null for Java compatibility. D’oh! C# does it exactly right, but only for value (“struct”) types, with a ? suffix on nullable types. Objects are still always nullable.

Some work has been done to paper over this in Java, including several disparate @NotNull/@NonNull/… annotations. The first problem is that these aren’t enforced by standard Java but only by third-party static analyzers (so I hear). The second is that they’re not the default, and they never will be without some massive shift in mindshare.

Static casts are another holdover from C. In C, they are how pointers change from one type to another. This is essential to much actual activity in C, in part because C lacks generics.

Take for example qsort(). This is a function that behaves very much generically: It takes a void pointer to an array of objects of a size provided as another argument, then uses a pointer to a function that compares two such objects, passed as void pointers themselves, to determine a sort order. There is no such thing as a “void” value; a void pointer could point to anything at all. You might pass an array of int, or perhaps an array of const char *. As long as the function you pass later is in agreement, the sort works equally well.

// Actual compare functions
int compare_ints(int a, int b) {
	return (a < b) ? -1 : (a > b) ? 1 : 0;
}
int compare_doubles(double a, double b) {
	return (a < b) ? -1 : (a > b) ? 1 : 0;
}

// What you pass to qsort()
// Note the explicit pointer casts
int compare_ints_vp(void * a, void * b) {
	return compare_ints( *((int*)a), *((int*)b) );
}
int compare_doubles_vp(void * a, void * b) {
	return compare_doubles( *((double*)a), *((double*)b) );
}

But who checks whether or not it does agree? The answer is nobody. You could accidentally pass a comparison for int with a compare function for double. The compiler will not catch it, because either function has the same signature, accepting two void *. By using an explicit cast, as the compare functions have to, you’re relieving the compiler of virtually all responsibility of checking that conversion.

And what’s the failure mode? An actual runtime error in the best of cases (probably a segfault), but often just reported success paired with some hard-to-track-down erratic behavior. Seeing a pattern?

Thankfully, the JRE (Java) and the CLR (.NET) do care about types. The result isn’t always pretty, but it’s far more deterministic: If an object is cast to something that it isn’t, an error (exception) is guaranteed. But in Java, it isn’t a checked exception, and C# doesn’t do checked exceptions, so if a cast fails in this way, you can catch it, but the compiler won’t make you. And, of course, it often defers what should be a static type error to run time.

Like with C, much of the use of explicit casts in Java is to simulate generics; type-erasing generics were added a couple of major versions ago, but code from before that era is still alive and kicking. For example, before Java 1.5, java.util.List<T> was available only as java.util.List, which essentially always behaved as java.util.List<java.lang.Object> does now. If you had a List containing nothing but String objects, you couldn’t express it, so instead of someList.get(n), you’d do (String)someList.get(n), counting on convention and luck for any type safety.[3] The addition of generics dealt with the worst of this usage.

Java and similar languages eschewed the C union, citing that its legitimate usages[4] had been supplanted by inheritance-based polymorphism.

For example, say you have a heterogeneous List, in which each element is a String, a Number, or a Date. The types and orders are not known at compile time, but it is known that any of these three types can be elements without a problem. There is no direct expression for this in Java. The polymorphic approach would be to use a List with an element type of the nearest common ancestor, which is Object. This will work, but the code will need to incorporate explicit logic to determine that each member is allowed, because the compiler now allows any Object, not just the ones we want. D’oh again.

In the process of doing this checking, there’s something else that feels a little loose. In essence, there is a sequence that should always result in sane cast usage. First[5], you check the type membership—if it is this type, jump to the second step; otherwise, skip the rest[6]. Second, you cast the reference. Third, you execute code that uses the reference.

The most important part is that if the first step is a negative, you never try to cast the reference and you never attempt to use the cast reference.

Here’s what a bunch of these look like chained together, along with a null check:

void doSomethingWith(Object o) {
	// ... code that doesn't care about the object type ...
	if(o == null) {
		// Run-time check :-(
		throw new NullPointerException();
	}
	else if(o instanceof String) {
		String s = (String)o;
		// ... handle the string ...
	}
	else if(o instanceof Number) {
		Number n = (Number)o;
		// ... handle the number ...
	}
	else if(o instanceof Date) {
		Date d = (Date)d;
		// ... handle the date ...
	}
	else {
		// Run-time check :-(
		throw new IllegalArgumentException();
	}
	// ... code that doesn't care about the object type ...
}

It shouldn’t be this painful to write sound code. In this matter, Scala is the only one that does it right. The important thing to note is that Scala’s pattern matching combines all three of the steps into one; the same syntax that checks the type also casts it for you and runs contingent code.

def doSomethingWith(o: Any) {
	o match {
		case s : String => ... // handle the string
		case n : Number => ... // handle the number
		case d : Date => ... // handle the date
		case _ => throw new IllegalArgumentException()
	}
}

The only thing I think could be better about the above code would be the ability the omit the default case. (You can, but the failed match is a run-time exception.) Alas; Scala doesn’t do union types[7] and it still allows null.

I’ve drafted multiple language specs to help heal these wounds while remaining essentially C-esque or Scala-esque in syntax[8]. Hopefully I’ll succeed in implementing one of them at some point…

  1. [1] like trying to call a method on an object or non-object that doesn’t implement it
  2. [2] function f(x){return 100+x} results in either 123 or “10023” for f(23) and f(“23”); generally only one of these is the expected behavior
  3. [3] The Java compilers that support generics literally just do this for you, for the sake of backward compatibility. It’s kooky, but I’m fine with it as long as it’s the compiler’s job.
  4. [4] It was popular to use unions to destructure a value’s in-memory representation, for example, a union of a uint32_t superposed on an array of four bytes, but this usage is non-portable and generally contraindicated.
  5. [5] If the reference happens to be declared volatile, copy to a nonvolatile variable before proceeding.
  6. [6] and, optionally, run code that only happens if the type check fails
  7. [7] A similar effect can be had from case classes, but it involves surrogate objects.
  8. [8] Lisp is a nice idea, I think, but the syntax is a little abhorrent to non-theoreticians.

Comments are closed.