Implicit safe conversions

Can we consider implicit widening conversions of int to long, float to double, etc.

val x = intValue.toLong()
val y = floatValue.toDouble()

seems somewhat redundant.

Yes, integer promotion is on our radar. No plans to implement it soon, though.

That is good news, but what about float promotion is that also on the radar?
And by not soon are we talking 6 months, 1 year or longer?

Not that it matters, once you go Kotlin you never want to go back!

What counts as a ‘safe’ conversion?

Trivial cases such as those shown above are clearly safe, but as soon as you add even the slightest complexity, there’s danger.⠀(The Java Puzzlers book has many examples of the gotchas lurking around numeric promotions.)⠀For example:

val l: Long = 100_000_000 * someIntValue

At what point is the promotion done?⠀The naïve approach would be at the point it’s assigned to the Long — but then the multiplication would be done in Ints, and could overflow before the promotion, even though the variable would be big enough to hold the final result.

(Alternatively, if all Ints were to be promoted before multiplication, that would cause other surprises in intermediate values.)

What about mixed-type expressions?⠀If you multiplied a Short by a Float, what type of arithmetic would be done, and what would be the result type?

What about the assignment operators such as += and *= — if it silently promotes the left-hand value before calculation, how does it handle overflow when demoting it back again for the assignment/

And what happens when the new unsigned typed get involved?⠀Those bring in a whole new range of nasty issues.⠀What type do you get if you multiply a signed number by an unsigned?⠀(What value do you get??)⠀Java’s designers decided not to include unsigned numbers precisely to avoid this hornet’s nest!

There are a few cases which are clearly safe, such as strict widening promotions when passing a function argument.⠀But even there, what if you tried to pass an Int expression such as the multiplication above to a Long parameter?⠀Your expectations might differ depending whether you knew that the parameter was a Long, and whatever the language did could be surprising to someone.

Kotlin’s current approach of requiring explicit conversions everywhere is uncharacteristically long-winded, but it’s characteristically clear and safe: it avoids all the nasty gotchas that lurk in languages like C and Java — and it forces you to think through the issues.

The only completely safe improvement would be to make explicit conversions more concise.⠀But how could that be done in an elegant way?⠀The existing .toLong() &c methods follow the existing naming convention; they’re obvious, easily discoverable, and trivial to infer.⠀You’d either need a shorter method name (which would be cryptic and ugly), or some new syntax (ditto).

The inclusion of bitwise operators in the standard library, instead of the core language, shows that we do less low-level bit-twiddling now than we did a couple of decades before (when Java was released).⠀Similarly, we probably don’t do as much complex calculation — we tend to code at a higher level these days!⠀So the overhead of explicit conversions is less than it would have been a few decades ago.

Please don’t add a lot of new gotchas to the language just for the sake of a few trivial safe cases.

2 Likes

Hi gidds,

All very valid points and I agree that maintainable code that conveys a developers explicit intent trumps all other considerations, however take the following scenario:

val hull = ConvexHull().build(model.extractPoints().map { (x, y, z) → Point3D(x.toDouble(), y.toDouble(), z.toDouble()) })

val hull = ConvexHull().build(model.extractPoints().map { (x, y, z) → Point3D(x, y, z) })

The graphics engine uses Float32 for performance but setup phase, such as convex hull generation, use Float64 for additional precision and stability.

The second line is more compact and I don’t think it suffers from any nasty side effects.

For stuff like that I gennerally like to just add my own custom overrides.

fun Point3D(x: Float, y: Float, z: Float) = Point3D(x.toDouble(), y.toDouble(), z.toDouble())

For overriding constructors I just add an annotation to suppress the “name should start with lower case letter” warning, can’t remember it’s name right now. Intellij has a quickaction to add it.
Or if you think it’s save for your usecase you can even use something like

fun Point3D(x: Number, y: Number, z: Number) = Point3D(x.toDouble(), y.toDouble(), z.toDouble())

Sure you have to go though and add those overloads to your classes but this is not too much work.

1 Like

Yep, that’s what I’m already doing, it’s not a major thing just a nice to have for safe widening ops, but I understand it’s low on the Kotlin enhancements pipeline, it’s on the radar so will track along my Kotlin journey.

Kotlin does not appear to require explicit conversions everywhere, the following compiles and runs without any warnings or errors:

    val i: Int = 1
    val l: Long = 4
    val f: Float = -0.30826f
    val t: Float = f * 4 - 1
    val d: Double = l - i / 6.0 + f - 0.38348
    println(t)
    println(d)

Yes, rules for literals and variables are different. Homogenization is one of goals of the promotion.