Casts as operator functions

Many classes in Kotlin have methods that “cast” an object to another type, like this:

class A(val something: String) {
    fun toB() = B(something)
}

class B(val something: String)

fun doSomething(b: B) = println(b.something)

Then doSomething must be invoked like this:

val a = A("blafoo")
doSomething(a.toB())

I believe it would be a great addition to the Kotlin Language to be able to provide these casts as operator (or other special) functions. The A class from the example above would have a function like one of these:

operator fun toB(): B = B(something)
cast fun toB(): B = B(something)

The operator example has the disadvantage of introducing dynamic names (“everything starting with ‘to’ is a cast”) to operator functions because the cast functions would only differ in return type otherwise.
On the other hand the cast example would require introducing a new keyword and add more complexity than just another operator function.

For the actual usage of the operator one of these three solutions would be used (only one would be implemented):

1. Smart Casting:

val a = A("blafoo")
doSomething(a) // "a" is smart casted to B

Here the compiler would first look for a doSomething function that takes A as a parameter, and if not, try to find one for the provided casts. This is the least verbose method but can get very complex and lead to errors.

2. Explicit Casting:

val a = A("blafoo")
doSomething(a as B) // No error, because B is provided as a possible cast for A

3. A new keyword

val a = A("blafoo")
doSomething(a to B)

As one could argue that the transition from A to B is not really a “cast”, a new keyword could be introduced.

Kotlin explicitly decided against smart casting so option 1 is out. Option 3 would need a different keyword since to is already an established function to create Pairs for maps, etc.

That leaves option 2 which will also not work. Kotlin has an established meaning behind foo.asX() and foo.toX(). The first is a cast/creates a wrapper object. If you change foo you also change the result of asX() and vice versa. toX() on the other hand creates a new object with an independent state. Therefor using the as operator for those functions is a bad idea.

3 Likes

Looking at the second and third option, I think there isn’t enough benefit over the currently available options to motivate a change–because IMHO there isn’t any change proposed by those two options.

Both the second and third options have the use of: <input type> <some conversion word> <output type>
Currently, explicitly converting a type can be done by making an extension function (or normal function). Your proposal only adds a keyword to this function and maybe change the way the conversion method is called.

Option two does differ from option three in that is hides when a cast is a raw cast or a conversion method call. So that part does differ more than just adding a keyword.

Groovy has facilities to do this kind of cast overriding to actually perform a conversion–although through constructor argument types. Personally I see the value Groovy gains from this shows up primarily in trailing method conversions such as "my string" as File. If Groovy had an easier way of declaring extension functions, I suspect more of their conversions would be done with .toType() or .asType() like in Kotlin.

Option one is requested from time to time since some people have used type coercion in other languages. IMO I’m not in favor of this conversion being done at on method calls implicitly. I find an explicit conversion better for humans than the reduction in characters.

As @Wasabi375 mentioned, Kotlin has made use of the toType and asType conventions. In some cases they provide more flexibility for conversions that could require arguments (such as String.toInt(radix = 10)). Another benefit to using methods for conversion is that they fall into other API conventions like the use of orNull in String.toIntOrNull() (although maybe this isn’t the best example since we have null-safe casting as? ;p)

In Kotlin using the as operator is a low level thing that only types an object as one of it’s supertypes or subtypes–any complex conversion or derivatives of that object are currently done by method calls. Allowing type coercion with the as operator means users must consider a lot more. For example, contracts and smart casting would break for every use of casting due to a successful cast providing no information about the source object.

Maybe you have some specific use cases that demonstrate the benefits or pain of the currently available options? (I would link to other type coercion discussions or issues but I’m on mobile)

I think a use case (for the first option) would be to have a class like BigInteger that can be created from an integer. You could create an extension function toBigInteger() for the Int type:

operator fun Int.toBigInteger(): BigInteger = TODO()
fun someFun(big: BigInteger) = println(big.toString())

someFun(1)

Here the function can be invoked with 1 because the conversion from Int to BigInteger exists, which looks much cleaner without the explicit call to toBigInteger() in my opinion.

There is not much pain in using the established methods though, and the proposal is of course only a very minor improvement.

Another way to solve this problem would be to just create a second someFun that takes an Int, but I think this would be unnecessary Boilerplate code you have to repeat for every function that takes a BigInteger as its argument, and I would rather just define once that an Int could also be used in place of a BigInteger.

I think the proposed system still differs from other languages in that only explicitly defined conversions are allowed, so no conversions like from a Boolean to a File (except of course you, for whatever reason, explicitly allow that :wink:).

I meant “smart casting” in the first option as in the kotlin docs. In addition to the explicit casts and is-checks the compiler could also take into account the defined conversions for the type at hand.

Casting and converting are 2 very different operations. Some languages have treated the 2 concepts as if they were the same thing and created a ton of confusion.
When you cast x to type Y that succeeds if x was already of type Y, there is no real change here, you just are telling the type system something it didn’t know for some reason.
When you’re converting something you’re creating a new object entirely. When you convert an Int to a BigInteger you’re allocating memory and running a constructor.

Kotlin is very strict in that no conversions happen automatically: an Int is not a Long, same goes with Float and Double. That way all conversions are explicit and it’s easier to tell what types you’re really using.

2 Likes

I see your point for allowing more convenient to write code using methods with BigInteger by not having to write out any explicit converting at the call site. My concern is that it is harder to read without possible confusion.

Under the saying of “scripture languages should cater to the writers of their source code while application languages should cater to the readers of their source code” I would be concerned about the loss of readability outside of the simple examples.

True, you must add explicit convertibility to an object, but when I say implicitly conversation I mean from the callers side. When a caller used someInt(1) they are calling a method with an implicit conversion that is explicitly defined elsewhere.

Another potential complication is method resolution. Even if we make a rule that calls that don’t require conversion always resolve before ones that require conversion, there still remains a big mess every time an API adds an overloaded method since every overloaded method has the potential to be a breaking addition.

For an example for a confusing conversions–which was removed–look at how the conversation for Double.toShort() was deprecated because it performs conversion without being clear what should happen for the caller.

For breaking smart casts, I can imagine smart casts still working as if the object is some kind of intersection type of all it’s conversions–which would be pretty messy and still soft break smart casting for a lot of cases since we aren’t really learning anything about the cast object itself from a successful cast.

Just saying, this is very much starting to get into Scala’s implicits territory, which probably isn’t ideal. However, I’d urge you to check out Arrow’s Type Proofs plugin since their @Coercion feature sounds exactly like what you want.