2011-03-05

Type Coercion

A type conversion is an operation that takes a value of one type and returns a value of another type such that the two values are somehow equivalent. Examples include:

  • float(5)5.0 // integer to float
  • string('s')"s" // character to string
  • codepoint('A')0x41 // character to integer
  • A (Java) FileInputStream converted to a ByteArrayInputStream by first loading into memory

If no conversion operator explicitly appears in the code, the conversion is called a type coercion. Like all implicit operations, coercions can be convenient (less source code to type and read), but also dangerous (as things happen without the programmer's explicit authorization).

How can languages make conversions and coercions available to programmers? Coercions are usually dealt with in one of three ways:

  • Allow all corecions
  • Disallow all coercions
  • Allow only certain coercions, such as subtypes to supertypes

No Coercions Ever

Languages like Ada and ML have a very simple rule: no coercions ever. One cannot even use an integer where a floating point value is expected, without explicitly casting the integer to a float.

Implicit Upcasting

Some languages, such as Java, allow casting in only one way, from subtypes to supertypes. This is called upcasting. You can use an integer where a float is expected, and the integer value will be coerced to the right floating point value. You can't go the other way automatically, though.

Implicit Downcasting

Coercions from a supertype to a subtype (downcasting) are inherently dangerous because information is being lost. In C:

int x = 8.2;            // x gets 8
int y = 8.5;            // does y get 8 or 9?

Directly assigning a floating point value to an integer variable in its declaration is unrealistic; here's something that is more likely to occur:

void f(char* s, int n) { ... }
.
.
.
f("some text", z);   // z is a float, coerced to a int within f

Again, this may not be too bad in practice; z will be truncated or rounded and the resulting value will be passed to the parameter n. Unless z is a global variable and is assigned the value of n within f, there's no problem, right? People would never do such a thing, right?

Never say never! SAP has a product called Business Objects Data Integrator (BODI), with an embedded database programming language. In the Data Integrator Reference Guide, on page 453, they describe this function:

ifthenelse: Allows conditional logic in expressions.
Syntax
ifthenelse(condition, true_branch, false_branch)
Return value
true_branch or false_branch
Returns one of the values provided, based on the result of condition. The data type of the return value is the data type of the expression in true_branch. If the data type of false_branch is not convertible to the data type of true_branch, Data Integrator produces an error at validation. If the data types are convertible but don’t match, a warning appears at validation.

"The type of the return value is the datatype of the expression in true_branch"? That's not exactly intuitive. Combined with the fact that BODI (or the underlying database it may use) does implicit coercions from floats to integers, users are opened up to the following disaster:

P.latitude = ifthenelse(P.latitude is null, 0, P.latitude)

This is an attempt to clean up a null value by setting it to 0. But assuming the latitude is a regular floating point value, this assignment destroys the value, turning 43.2, say, into 43. If latitudes and longitudes are both "adjusted" this way, places in good neighborhoods end up in bad neighborhoods or in the middle of a river or ocean.

Coercions and Type Systems

Why would BODI even create an ifthenelse operator that would coerce an argument? This is a classic case of mixing static typing with weak typing. Note:

  • Static Typing means that all expressions must be fully typed at compile time, while dynamic typing means that types are resolved at run time.
  • Strong Typing means that operators applied to arguments of the wrong type generate errors, while weak typing means that the values will be coerced (wherever possible) to make the operation valid.

Static typing goes well with strong typing (ML, Ada), while dynamic typing can be combined with either strong (Ruby) or weak (Perl, JavaScript) typing, without too many surprises.

How should BODI's ifthenelse operator have been defined? Using static and weak typing together is not really the worst element at play here; it's the implicit downcasting of P.latitude to an integer. Why did they do this? Perhaps the designers of this language thought it would be "too complicated" to define the return type as "the most general type of true_branch and false_branch" and thought it "too restrictive" to force the two types to match exactly. They could have at least detected the fact that the false_branch had a more general type then the true_branch and generated and error there. Perhaps they thought that "too confusing." But wait, taking the type of true_branch as the result type is asymmetric and confusing on its own! How is that intuitive?

Note that with dynamic typing this problem never would have arisen since the ifthenelse expression would have produced either its second or third argument and hence taken on the type of the value returned, whatever that was. A dynamically typed language would never have even considered a coercion in that case at all! And a statically typed language with strong typing would have rejected the expression outright.

Static + weak is the problem. Language designers would be wise to avoid this combination.