```typealias Alias<T> = T Type alias expands to T,...
# getting-started
r
Copy code
typealias Alias<T> = T
Type alias expands to T, which is not a class, an interface, or an object
I need a way to get this working.
j
Why do you need this?
What do you mean by
T
is not class, interface, nor object?
T
is a type. Do you need to constrain
T
to some types?
r
@Joffrey
Copy code
typealias Fahrenheit<T> = T

class Context (
    val temperature: Fahrenheit<Float>
) {
    fun fn() {
        return temperature * 2f
    }
}
its just a semantic alias
a way to distinguish if a number represents celsius or fahrenheit
I tried this
Copy code
typealias Fahrenheit = Number

class Context (
    val temperature: Fahrenheit
) {
    fun fn() {
        return temperature * 2f
        // now this breaks! 
        /*
        Unresolved reference. None of the following candidates is applicable because of receiver type mismatch:
        public inline operator fun BigDecimal.times(other: BigDecimal): BigDecimal defined in kotlin
        public inline operator fun BigInteger.times(other: BigInteger): BigInteger defined in kotlin
        */
    }
}
you can see that the same annoyance I always complain about with Kotlin not supporting operations under Number is breaking this.,
j
I can see your insistance on using
Number
where everyone told you you shouldn't 😊
r
I am fine with using Float i just want a way to annotate that this is representing fahrenheit
I don't want to have to do this
Copy code
typealias FahrenheitFloat = Float
typealias FahrenheitInt = Int
typealias FahrenheitDouble = Double
it would be much nicer to have a single type alias and then just specify the type of number as a generic argument
typescript allows this
j
For your use case, you most likely want a value class rather than a type alias. You don't want arbitrary values to be acceptable for farenheit or celsius, and rather ask for a conscious decision to create a farenheit value from a literal when needed. This way you can have type safe operations
r
OK yes that is true for temperature specifically but it is not generally true
things like length measurements do not have upper or lower bounds
those deserve to be raw primitive numbers
not wrapped in classes
Copy code
val distance: Feet<Float>
val distance2: Feet<Int>
val distance3: Miles<Int>
this is perfect if Kotlin would just trust me that
Copy code
typealias K<T> = T
is reasonable in my use case
j
But why do you need to handle all these types of numbers for farenheit? If you're going to consider them like decimal numbers anyway, why not just implement your things for double? At your API surface, you can then offer convenience and type safety via a value class
Wrapping in classes has nothing to do with adding bounds. It's about type safety. 20 farenheit cannot be added to 20 kelvin. 20 meters cannot be added to 20 liters. Representing those in value classes allow to add such type safety, and also use primitive types under the hood most of the time so don't get any perf hit
👆🏻 1
Your type aliases wouldn't add any safety, they are really just aliases, so people could pass any other double where a farenheit is expected
Even a liter or a meter
r
Sure, but that is the sort of conceptual error we already have the possiblity of when programmers are allowed to do things like
Copy code
var age = 15
var countries = 52

countries += age
so I just think - be good at progrmaming.
value classes are very cumbersome
I do not want to use them constantly
if the language had a better way to implement them I would use them
but right now, extension functions are fantastic
I can do 15.seconds
that is so much better than having to wrap a fake Second class
j
How about an extension property returning your value class?
r
that is a cool idea
and then I can override arithmetic operations on it?
so it looks and feels like a real number?
j
Yes
r
and I can just take in Float | Int | Double as the generic argument to the class?
j
Yes. EDIT: I don't mean as a generic type parameter, but as a contructor argument, so you can construct instances with any of those types, and use a single type internally.
r
Oh perfect
j
And you can play with dimensions like 20 meters / 10 seconds = 2 meters.s-1
r
Thanks a ton this is great
j
You're welcome!
r
@Joffrey
Copy code
@JvmInline
value class Fahrenheit<T>(val value: T) {
    operator fun plus(other: T): Fahrenheit<T> = Fahrenheit(value + other)
}
this gives error on then
+
j
Make it non generic, use double internally so your + operator makes sense
With any
T
, + doesn't exist
r
When I do
15.seconds
, that will then turn into
15.0
isn't that deceptive?
and I would actually want to do
Float
instead since this is for a physics simulation thing so memory is more important than precision
but if i do Float, it prevents me from being able to do a
Double
in some other rare times
the appeal to me of value classes was that I could pass in a generic argument
but now it looks like this is the same as just doing
Copy code
typealias Fahrenheit = Float
but, this looks like it is going to be expensive..
i will be creating fahrenheit numbers or adding to them potentially thousands of times a second
got it working
j
When I do 15.seconds, that will then turn into 15.0. isn't that deceptive?
Not really. What happens internally and your public API are different things. You can definitely store doubles internally, and do all operations with them, and then expose whatever operations in your API using other number types if they make sense.
r
wait i have modified it
Copy code
value class Fahrenheit<T: Number>(val value: T) {
    operator fun plus(other: Fahrenheit<T>) = Fahrenheit<T>(value + other.value)
}
i don't understand why this doesn't work?
T are guaranteed to be the same kind of number
j
> the appeal to me of value classes was that I could pass in a generic argument The point of value classes is rather the type safety. By saying "these are ferenheit degrees", it no longer matters to the users whether internally you store a float, a double, or an int. You just have to decide about the underlying representation based on how you want the system to perform behind the scenes (maybe float in your case). The
plus
operator is not defined on
Number
. Why do you want to store farenheit integers sometimes?
r
yeah but it does matter in the context of physics sims and video games
float vs doubles have performance impacts
j
Yes, sure. Use floats if your system requires floats. But why do you want to use floats AND integers for the same thing?
r
The
plus
operator is not defined on
Number
.
Yeah but I don't want
number
to be a generic argument. I am trying to say "you can only pass in Float, Int, or Double for the generic argument"
Yes, sure. Use floats if your system requires floats. But why do you want to use floats AND integers for the same thing?
Most of the time I want floats. Some times I want doubles. Time is an example. Most time variables, like delta time, only require float precision. But elapsed time requires double
I want to be able to say 15.seconds for both
or 15.0.seconds and 15f.seconds
or distances. At the scale of a galaxy simulation, I actually need to use doubles for distances. Whereas for most normal sim stuff I only need floats
j
The API surface to create values is kinda orthogonal to this discussion. The discussion at hand is whether it's ok for you to have a single internal representation. There can be considerations like do you want to allow
Farenheit<Double> + Farenheit<Float>
?
r
sure but I am fine with this not allowing addition of different types
if I commit to float for the single internal representation then I lose my ability to have precision in the few cases where I need it. If I commit to doubles, then I am paying a much higher performance price than I need for the vast majority of cases
j
Then you could just use different types for those. If you have high-precision cases, use your high-precision
Farenheit
or
Distance
. You can do that too. The operators in it will be different operators (even though they look like
+
they are different `+`s, so it's not as redundant as you might think
> then I am paying a much higher performance price than I need for the vast majority of cases Did you measure this, by the way? I kinda doubt you actually pay a price for using doubles
r
i googled float casting and SO said it is expensive
j
Without casting, just using doubles
r
game dev forums are unanimous that doubles are generally bad for video games because of how much space they take up
j
On the JVM platform?
r
I'm not sure. Just in general, video games are very intensive. You are doing hundreds of thousands of calculations every second or so
and storing hundreds of thousands of entiites
potentially
Copy code
@JvmInline
value class Fahrenheit<T>(val value: T) {
    operator fun plus(other: Fahrenheit<T>) = Fahrenheit<T>(value + other.value)
}
Fahrenheit<Float> + Fahrenheit<Float> should work...
Float does implement
+
but there is not a way to tell Kotlin that
T
will be a Float | Int | Double
j
But the
Farenheit
class has no reason to believe you will not use
Farenheit<String>
, hence why
+
is forbidden here
r
can I constrain the generic at all
Copy code
interface Arithmetic {
    operator fun plus()...
}

@JvmInline
value class Fahrenheit<T: Arithmetic>(val value: T) {
something like this
j
there is not a way to tell Kotlin that T will be a Float | Int | Double
Even if there were, it wouldn't help. As I said, the
+
operator is not the same on these types. They don't implement a common
Addable
interface or something, so you cannot call a virtual
+
operator and get different implementations for each of these types. You would basically have to create your own
+
operator on your number, and use a switch to use the specific
+
in every case
r
Float
+
Float
will work
this would not be a mixing scenario
j
I'm not talking about mixing types here either. I'm talking about writing the
+
symbol once and expecting it to refer to different functions.
r
you use the same
T
throughout the class. So if you instantiate it as Fahrenheit<Float>, then it's + operator will only operate on other Floats
j
Yes, but which
+
operator does the
Farenheit
class (as a WHOLE) use? Why would it match the one from
Float
?
r
because Float is T
and the parameter of the operator overload is T
j
I know what you're describing. But that's not how things work. The compiler takes your
Farenheit
class and compiles it. Now it has to compile the
+
operator you used between your
T
instances without knowing
T
.
r
ok so then there is not a satisfing solution to this
Time needs to be representable both in terms of floats and doubles
and I'm not going to have two separate value classes for that
I want to say "seconds" for all of them
j
Why not? If you consider them vastly different in terms of performance profile, and you don't want to combine them in operations, then you SHOULD definitely use different types
r
You are suggesting to maintain 4 different classes
Copy code
SecondsInt
SecondsFloat
SecondsDouble
SecondsLong
?
Copy code
@JvmInline
value class Fahrenheit<T>(val value: T) {
    operator fun plus(other: Fahrenheit<T>) = Fahrenheit<T>(value + other.value)
}
If kotlin worked how i expected, this would prevent mixing value types in operations.
j
I don't see a reason to maintain 4, no. You made your argument for using
Float
over
Double
internally yet still allow using
Double
internally in places where high precision is required. You haven't talked about using integer numbers besides the convenience of creating values, which doesn't require new types.
r
Okay but so you are suggesting SecondsFloat SecondsDouble then yeah
j
Yes, if you have 2 different kinds of specific needs in the same application. Although I would assume most applications would pick one or the other, and don't need to deal with both.
Also, performance of float is sometimes worse depending on the data and the hardware. So I wouldn't rush to use float before measuring a performance improvement
r
but do you concede that if Kotlin were a bit smarter in being able to infer
T
in the relevant scenarios, then
Copy code
@JvmInline
value class Fahrenheit<T: Arithmetic>(val value: T) {
    operator fun plus(other: Fahrenheit<T>) = Fahrenheit<T>(value + other.value)
}
would be a scalable solution
I would only need to define the operations once, instead of twice
maintain only one class instead of 2
since they will be entirely identical except for their math operation data types
Copy code
@JvmInline
value class FahrenheitFloat(val value: Float) {
    operator fun plus(other: FahrenheitFloat) = FahrenheitFloat(value + other.value)
    operator fun minus(other: FahrenheitFloat) = FahrenheitFloat(value - other.value)
    operator fun times(other: FahrenheitFloat) = FahrenheitFloat(value * other.value)
    operator fun div(other: FahrenheitFloat) = FahrenheitFloat(value / other.value)
}
@JvmInline
value class FahrenheitDouble(val value: Float) {
    operator fun plus(other: FahrenheitFloat) = FahrenheitFloat(value + other.value)
    operator fun minus(other: FahrenheitFloat) = FahrenheitFloat(value - other.value)
    operator fun times(other: FahrenheitFloat) = FahrenheitFloat(value * other.value)
    operator fun div(other: FahrenheitFloat) = FahrenheitFloat(value / other.value)
}
This is a lot of boilerplate 😕
just to work around an insufficiency in the type analylzer
j
Kotlin as a language is not limited in that sense. With your own class hierarchies you can definitely do that, with classic polymorphism. If primitive number types implemented your
Arithmetic
interface, you could definitely do that. What you're missing here is this common interface for primitive numbers. That said, if you had this interface, you would probably use it here, and most likely your performance concerns over float vs double would be dwarved by boxing and virtual dispatch, and would be better off using double everywhere.
The type analyzer works correctly.
Int
,
Float
,
Double
don't implement your
Arithmetic
interface
r
replace arithmetic with whatever like
Number
k
r
the fact is that me and you in this conversation can know that
Fahrenheit<Float>(5) + Fahrenheit<Float<(5)
will definitely succeed
but kotlin does not know that
so there is an inadequacy in what kotlin is able to know
or what knowledge it's able to express
j
me and you in this conversation can know that Fahrenheit<Float>(5) + Fahrenheit<Float<(5) will definitely succeed
I don't. I don't know myself what
+
you expect to be used in
Farenheit
, unless you define this general
+
somewhere.
r
we have no way to annotate T in a way to express this
j
You could also pass another class that is able to perform operations with
T
r
I don't. I don't know myself what
+
you expect to be used in
Farenheit
, unless you define this general
+
somewhere.
the + that is built into
T
and we pass in
Float
for T
both times
j
the + that is built into T
There is no such thing for any
T
. What if you pass
InputStream
as
T
?
r
yes we need a way to constrain T to only one of 4 number types
You can also use
Number
as a backing value, and then use
when
everywhere in your implementations to define the behaviour
Or you could use an interface for Farenheit, and have different implementations.
r
I want to use Number as a constrainer for Int | Float | Double
Kotlin just does not support unions
👍 1
j
No matter what you choose, you'll have to specify somewhere the different calls to different
plus
operators
Indeed Kotlin doesn't support untagged unions (it does have tagged unions via sealed classes). But again, even if it did, there is nothing you could do with a value of the union type besides checking every case (the
when
approach I was mentioning), or using a common interface (which doesn't exist for
Int
,
Float
and
Double
). Kotlin doesn't use duck typing.
About this: https://kotlinlang.slack.com/archives/C0B8MA7FA/p1714989331970189?thread_ts=1714982954.165899&cid=C0B8MA7FA Let's highlight the fact that these operators are unrelated to each other by giving them names that don't look like each other (I think that's where your confusion comes from):
Copy code
kotlin
@JvmInline
value class FahrenheitFloat(val value: Float) {
    operator fun plus(other: FahrenheitFloat) = FahrenheitFloat(value addFloat other.value)
    operator fun minus(other: FahrenheitFloat) = FahrenheitFloat(value minusFloat other.value)
    operator fun times(other: FahrenheitFloat) = FahrenheitFloat(value timeFloat other.value)
    operator fun div(other: FahrenheitFloat) = FahrenheitFloat(value divFloat other.value)
}
@JvmInline
value class FahrenheitDouble(val value: Double) {
    operator fun plus(other: FahrenheitDouble) = FahrenheitDouble(value plusDouble other.value)
    operator fun minus(other: FahrenheitDouble) = FahrenheitDouble(value minusDouble other.value)
    operator fun times(other: FahrenheitDouble) = FahrenheitDouble(value timesDouble other.value)
    operator fun div(other: FahrenheitDouble) = FahrenheitDouble(value divDouble other.value)
}
There is almost 0 duplication in this code, despite appearances. I hope this makes things slightly clearer.
Note that you can definitely do the following:
Copy code
kotlin
@JvmInline
value class Fahrenheit<T: Number>(val value: T) {
    init {
        check(value is Int || value is Float || value is Double) { "The value must be an Int, Float, or Double" }
    }
    operator fun plus(other: Fahrenheit<T>) = Fahrenheit<T>(value + other.value)
}

@Suppress("UNCHECKED_CAST")
private operator fun <T: Number> T.plus(other: T): T = when {
    this is Int && other is Int -> (this + other) as T
    this is Float && other is Float -> (this + other) as T
    this is Double && other is Double -> (this + other) as T
    else -> error("Unsupported operand types ${this::class.qualifiedName} + ${other::class.qualifiedName}")
}
But honestly I don't find it much better than the straightforward approach. Again, the different
plus
operators have to be defined somewhere, and in that case it's in the
when
.
One thing that Kotlin as a language could improve here is to provide an implicit interface for all types implementing a given operator (e.g.
Plusable
,
Minusable
,
Divable
,
Timesable
, or something). But again, you would then use it in your case here, and this is most probably less performant than just using double everywhere for your use case.
r
@Joffrey is that check version a runtime thing?
will it have performance impact
t
One thing you should be aware of that I don't think was covered is that typealias does not in any way give you the kind of type-safety you want: https://pl.kotl.in/tTtJ8LFJW -- you can add Celsius to Meters if they're both aliased to Float. Typealiases are "lightweight" and don't hide what they alias from the typechecker.
(I think this was mentioned but not made explicit this way. Apologies if I'm repeating something that was already covered.)
j
I think it doesn't hurt to repeat/paraphrase things, so thanks in any case Tim! 🙂
is that check version a runtime thing?
@Ray Rahke yes that's at runtime, so yeah there is in theory a tiny overhead in every constructor invocation. But it would require measuring to see if it actually is noticeable
r
Okay, thank you all. I will work with what is possible