6
$\begingroup$

Many languages that has static typing or type hinting, support user-defined casting functions that are called automatically when a function expects another type that the parameter could be cast into. This is user-defined implicit type casting.

I asked a question about a specific type of implicit casting. But apparently the idea of implicit casting itself is not liked by everyone. And there wasn't a question about that. So I'm posting this.

What are the pros and cons of supporting user-defined implicit type casting?

Built-in casting such as from derived types to base types is not part of the question. There are also many dynamically typed languages without type hinting, which don't have a place to use implicit casting and they are also out of the question.

$\endgroup$
1
  • $\begingroup$ Implicit type conversions are still very much a thing in dynamically typed languages without type hints; for example, 1 + "foo" in Javascript coerces 1 to a string. $\endgroup$
    – kaya3
    Commented Jul 26, 2023 at 9:32

4 Answers 4

6
$\begingroup$

This is a trade-off between making the language convenient to write in vs. making it easier to avoid bugs caused by using the feature incorrectly or unintentionally (i.e. not creating footguns).

Most languages allow implicit type conversions (coercions) between at least some types in some contexts. For example, Python doesn't coerce x to a string in code like "Hello, " + x, but it does coerce numeric types in arithmetic expressions. Rust coerces in neither of those situations, but still silently converts between various types in some other places ─ including some user-defined conversions (notably, for the ? operator). It would be rather inconvenient to require an explicit cast like (obj as &T).method() just because obj is &mut T instead of &T, when method expects &self instead of &mut self, for example.

The simple truth is that not all coercions are equal: some are extremely convenient while being very unlikely to cause problems, whereas other coercions are only useful in specific contexts and it would probably be a mistake to apply them in other contexts without the programmer's explicit assent. So each language makes this judgement many times, not just once; it's less a question of whether to allow coercions or not, and more which kinds of expression should be coercion sites and which kinds of coercion should be performed at each coercion site.

When it comes to user-defined coercions, the considerations are mostly the same ─ instead of thinking of it as "yes or no", think of it as "which coercion sites?". That said, there are a couple more things to think about:

  • One reason Rust allows users to define the coercion behaviours for their own types (through traits like From/Into and Deref) is because of a design philosophy that, as much as possible, the built-in types shouldn't be special cases in the language. So for Rust it's not just about convenience, but also about allowing the user to forego as much of the standard library as they want in favour of third-party alternatives.
  • For avoiding bugs, an important principle is that the behaviour of a piece of code should not be changed by modifying seemingly-unrelated code. So user-defined type coercions should probably not be allowed when the user doesn't "control" either the source or the target type, as this could allow code written by the user to (accidentally) insert coercions into code that doesn't make use of any code written by the user.
$\endgroup$
3
$\begingroup$

Uniformity

Many languages have some sorts of implicit casts built-in. Derived to base class is one example, but there's also bool coercion, integer widening, boxing, nullptr-to-pointer, format string literal to string (in C#) and Never-to-anything (in Rust), just to name a few. These could be implemented as compiler-level exceptional cases, but they could also be implemented in the standard library via a general implicit cast mechanism. The latter simplifies the language, which is in my opinion a desirable property.

The problem with implicit casts is mostly their misuse. Hiding a costly operation in one, for instance, can be very annoying in terms of code clarity and predictability. I do believe, however, that the solution is to discourage implicit casts, but not forbid them, same as with operator overloading. For example, the construct used to define an implicit cast operation could involve significant amounts of boilerplate code (like, perhaps, implementing an ImplicitCastTo<T> trait, rather than just annotating a method with an attribute), dissuading the users from picking this as the simplest option

$\endgroup$
1
  • 1
    $\begingroup$ This, to me, is a very strong reason. If you allow implicit casting for your built-in types but not user defined types then it makes those types second class citizens in the language. $\endgroup$ Commented Jul 26, 2023 at 15:58
2
$\begingroup$

What is User-Defined Casting?

What does that mean at all? It's a way to call some user-defined method, getting one input parameter and returning some different result. If we call this method a "casting method", we assume that the result somehow represents the same entity.

This assumption is nothing more than a convention, and there is no way to strictly check whether a given 1-input/1-output method qualifies as a cast. So, technically, it's nothing more than an additional way to write down a specific type of method call - or even not write it down and have the compiler find the method in the case of implicit casting.

Explicit-Cast Syntax for User-Defined Methods

Why would you want to have an additional explicit syntax to call a user method? You surely have one already, so it's syntactic sugar at best. Let's compare:

B b = (B) a;          // using cast syntax
B b = a.ConvertToB(); // using classic method-call syntax

IMHO, the cast-syntax version does not provide better readability - it hides what is happening.

And the concept of casting is already used for lots of different things in existing languages. E.g. in Java, B b = (B) a; typically implies that a statically is of some supertype of B, and to allow the assignment, the developer promises (by writing the (B) cast expression) that the runtime type of a will always be B or a subtype of B, and accepts getting a ClassCastException if this promise is broken.

So, overloading the cast syntax with even one more possible interpretation causes trouble - the user doesn't explicitly see what's going on, for the compiler it gets even more complex to decide what the (B) cast expression really means.

Implicit Casts with User-Defined Methods

This assumes that we already have the explicit-cast syntax for user-defined casting methods. Implicit casts mean that the developer may omit the cast syntax at all, and the compiler chooses the appropriate casting method.

Now any term in an expression is subject to a possible implicit cast. We already have that in many languages, and (together with overloaded operators) it is the cause of many subtle bugs. Think of

double x = Math.PI * 1/6;  // okay in typical left-to-right evaluation order
double x = 1/6 * Math.PI;  // zero in typical left-to-right evaluation order

The first line implicitly converts the 1 to double, has a double expression Math.PI * 1, converts the 6 to double, and does a double division.

The second line starts with an int division 1/6, giving zero, and converts that to double for the multiplication.

Having user-defined implicit casts is the invitation for lots of similar problems.

Assignment/Equality Consistency

I'd like the following to be true:

B b = a;
Assert(b == a);

If I assign something to a variable, I expect the value stored there to be equal to the original expression's result.

Allowing user-defined implicit casts breaks that expectation unless you also apply the casts in the equality comparisons. I didn't explore the implications of doing so, but I fear that it hurts the usefulness of the == operator. And do you cast the left-hand side of the == or the right-hand side or both?

$\endgroup$
1
  • 1
    $\begingroup$ The question is only about implicit conversions, but a lot of your answer addresses explicit conversions. The last part ("I fear that it hurts the usefulness of the == operator") should be explained with a reason other than your fear, and you didn't address the fact most languages which support implicit type conversions only do so in some contexts, not all (so B b = a; would not have a be implicitly converted in many cases). $\endgroup$
    – kaya3
    Commented Jul 26, 2023 at 9:54
1
$\begingroup$

Pro: Upgraded types shares the same simple values

Say the language itself supports only basic integers, and libraries has implemented high precision integers, rationals, complex numbers (of integers), they could use the built-in integers to represent the basic integers.

Pro: Allow defining internal layers

If something is upgraded to return a type that contains more information than before, it could return a new type that could be cast to the original type, and doesn't affect the old code too much. One example is to simulate properties in a language without properties. Another is for computation libraries to keep an expression before the result is requested.

Not helpful for bigger objects

It's usually costly to convert between bigger types. And some information such as references from another object might be lost. It makes sense for the programmer to track every such conversions. And interfaces are the right way doing things in most cases, making the implicit casting feature a bit confusing.

It could be a con if the programmer has to nonuniformly remember using type conversions for small objects and interfaces for big objects, with vastly different usage.

$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .