Ran into something interesting today: ```fun negat...
# javascript
b
Ran into something interesting today:
Copy code
fun negate(value: Any?) = when (value) {
   is Int -> {
      println(value::class.simpleName)
      -value
   }
   is Double -> -value
   else -> Double.NaN
}
Calling
negate(-5.6)
returns
-5
. It hits the
is Int
path! And the best part is, it prints "Double"
👌 2
g
I presume a
number
will returns true for
is Int
and
is Double
? If yes, it kinds of make sense, but a warning could be great 😄
b
I disagree about it making sense, I think
is Number
should return true, but the other's shouldn't, if Kotlin is storing them all as Javascript Number.
t
What happens if you just switch the order of the cases?
Does passing in 5 come through as a double?
yes black 1
And even if it did, it doesn't look like it would have any adverse side effects
g
From my understanding, Int/Float/Double... are implemented with a
number
in Javascript, it's not boxed (afaik) so checking the type doesn't make a lot of sense.
l
If it's able to print
Double
, then it should be able to go down the right path based on a fixed
is
check.
g
Yes ... or maybe it's just using the biggest precision available to avoid precision loss?
t
Maybe. Though coercing int to double can lose precision in some cases as well
đŸ€” 1
b
Any conversion from
number
will be lossy, that's why I believe it should only pass checks for the base type
is Number
, forcing the conversion to always be explicit & visible in the code. You want to treat the JS number as a Double? use
Number.toDouble()
, you want to treat it as an
Int
? Use
Number.toInt()
I can't imagine any good reason why
is Int
should be true for Javascript numbers...
✅ 1
t
Yeah exactly. Did you try switching the order of the cases?
I’m wondering if that makes a difference
g
Changing the order will not fix the issue: in JS those types are simply mapped to
number
and not boxed (I guess for performance reasons). You can experiment by yourself if you want https://pl.kotl.in/YX0Y9ytmM
t
You could
when
on the class's simpleName. 😅 But that's not exactly ideal
And multiplying by -1 is better but can still trigger a rounding issue with double
g
You could 
when
 on the class's simpleName.
A Int or a Double are going to be mapped to
number
, and
number
will always returns "Double" so not feasible. You just have to box yourself the value in a crafted class so you ensure it's not reduced to
number
.
Copy code
(12.34f)::class.simpleName == "Double"
Mmmh you were right, looks like it's actually looking for decimal places or not to return "Int" if there is no decimal part! But Float==Double
t
What about
if (number % 1 != 0)
g
image.png,image.png
t
Oh interesting. Using bitwise or 0 and then comparing that to the original number
That may beat the
if (number % 1 != 0)
in performance
There is no Kotlin or for Number or Double though 😕
g
%1 == 0
or using
toInt()
to compare like this :
Copy code
val value = 12f
    val a : Any = value.toInt()
    val b : Any = value
    println("is Int: ${a == b}")
    println("is Double: ${a != b}")
Both won't really work, because
12.0
for example is presumably considered as a Double by the developer, but actually is an Int for KotlinJS, because there is nothing behind the dot. (Once the value has been passed in an
Any
variable indeed.) Using a external on jsBitwiseOr() could work, cannot test that tho.
t
Well now that you have an Int, what happens if you use Kotlin’s bitwise or? similar to the direct jsBitwiseOr?
g
Looks like "Int.xor" ->
^
and 'Int.or' ->
|
, not sure if we can find it somewhere like that đŸ€”
I thought implems where different but actually jsBitwiseOr works like Int.or. But at the end it's still the same problem: we cannot detect if the decimal part is equal to zero or no, but we can't infer what was the original type, so the readability of this code is compromised.
t
I've never tried kotlins instance equality on a primitive, but I'm curious what that might do now
In jvm at least it seems to work with separate primitive "instances"
Heavy quotations on instances.
Yeah triple equals is not being any help either
Ok I was just assuming there was some magic with JS or and equals that could determine whether it was represented as a double or int. Doesn’t look like it, because the class for 12.0 is Int
So my colors are really showing, I’m a Kotlin guy who moved to JS. I didn’t know in JS all numbers are stored with floating point. Therefore, no conversion error from int to double. Just natural floating point errors. Also take a wild guess what class name this returns.
Copy code
fun main() {
    val d: Any = 12.2 - 0.2
    println(d)
    println(d::class.simpleName)
}
If we can literally not avoid the class changing from Double to Int here, with basic subtraction, I do not think it’s reasonable to attempt to preserve the type in your negate function.
If the 12.0's class going in is Int, then it should be fine going out as Int
Ok even more curious

Copy code
fun main() {
    val d = 12.0
    println(d::class.simpleName)
    val out = negate(d)
    println(out::class.simpleName)
}

fun <T : Number> negate(number: T): T {
    println(number::class.simpleName)
    return number
}
prints “Double” “Int” “Double”
Both on Node and Chrome, and both with new IR and without, Long uses a boxed type rather than number. 5 is represented as
{"_low":5,"_high":0}
So according to that, I wrote a version of negate using generics, because that preserves the concrete type, if available at the call site. Which preserves the output of 12.0 being Double, if it’s statically known on the input to be Double.
Copy code
fun <T : Number> negate(number: T): T {
    println("typeof: ${js("typeof number")}")
    val negated = when (number) {
        is Double -> -number
        is Long -> -number
        else -> error("Don't know how we will get here")
    }
    println("number: ${number::class.simpleName}")
    println("negated: ${negated::class.simpleName}")
    return negated as T
}
Copy code
println("input: ${input::class.simpleName}")
    val negated = negate(input)
    println("negated return: ${negated::class.simpleName}")
When input is
val input: Double = 12.0
, we get Double printed on the outside, typeof number, and Double on the inside. When the input is
val input: Int = 12
, we get Int on the outside, typeof number, and Double on the inside. (It is represented as a floating point in JS, so no precision worries here) When the input is
val input: Long = 12
we get Long on the outside, typeof object (or node, could be different based on environment), and Long on the outside. for 12.6 it returns correctly and we still get Double types all around. even tested with the tricky 0.1 that often gives rounding errors, and it came out with -0.1! Probably safest bet to go with
Copy code
fun <T : Number> negate(number: T): T {
    return when (number) {
        is Double -> -number
        is Long -> -number
        is Int -> -number
        is Float -> -number
        is Short -> -number
        is Byte -> -number
        else -> error("Don't know how we will get here")
    } as T
}
Even though testing shows only the first two are required. If the implementation of Kotlin changes at any point, this is hopefully more forwards compatible than just checking Double and Long. The order here does matter, Double needs to be checked before Int/Short/Byte etc because those will pass for all types represented by a JS number. The generics preserves the type of the input, and the unchecked cast should be safe.
👍 1
g
Copy code
val d: Any = 12.2 - 0.2
Just a side node for readers, be careful with operations like that, for example
0.1 + 0.2 - 0.3 > 0
, because operators are adding precision issues in the mix. Interesting to know that generics preserve the original type, so it all depends if you are going to use the negate method from Kotlin (with types known at compile time,
is
could be right) or from Typescript with Any or Number (then
is
will be... a bit unexpected). Anyway, I'll continue to think about number type check as a bad KotlinJS/multiplatform pattern, and I'll prefer boxing
number
in specific class when I need to use the type.
t
To be clear, all types except Long still pass the is Double check with generics.
The only difference with generics is that external to the generic function, it preserves any statically known type
Internal to the generic function, it acts like Number/Any
Another alternative could be checking is Long first, handling that with safecast, and then casting anything leftover to Double so you get access to the negate operator. This is safe now, but it breaks Kotlin’s encapsulation a little bit, and relies on implementation details I wouldn’t guarantee to be stable. There’s no way to do math with a number unless you cast it or type check it to something. So I think we have to use something unless we are going to avoid math entirely with TypeScript Any/Number interop