-
Notifications
You must be signed in to change notification settings - Fork 55
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Type-safe Units #75
Comments
If you have some vision, how it should look like, please present it. |
I was thinking about something similar to https://github.com/mipt-npm/gdml.kt/blob/master/src/commonMain/kotlin/scientifik/gdml/units.kt, but with singleton objects instead of enum. Meaning that each each unit has a conversion value relative to some default unit. Also, since we have different quantities, we can inherit those objects from an interface so it could look like this: interface TypeSafeUnit<U>{
infix fun convertTo(units: U): TypeSafeValue<U>
}
interface Length: TypeSafeUnit{
val value: Double
}
object Meter: Length{
override val value = 1.0
} The value could be resolved from type like this. Then we can do things like this: val meters = 1.2.m
val millilitre = meters convertTo Millilitre Also it should be possible to define composite units like velocity with appropriate conversion rules. I am not sure it is the best solution. We should study how it is done in other languages. @Shimuuar Your input is welcome. |
I think that main challenge (and main value) is to make things like P.S. I think conversion of meters to milliliters should give compile error and only work with cubic meters |
The Kotlin type system does not allow compile-time type algebra (and it is not such a bad thing). We will be able to hard-code composite type transformations for some combinations so the inline class TypeSafeValue<U: TypeSafeUnit<U>>(val value: Double)
operator fun TypeSafeValue<Meter>.div(other: TypeSafeValue<Second>): TypeSafeValue<Velocity> It is also possible to introduce composite unit types for thins like velocity, but it requires additional design effort. |
This approach means that every combination of dimensions should be written explicitly. That's ungodly amount of boilerplate. Take for example: |
What I've done in my POC is took Kotlin's Besides that, I don't see any possibility (this doesn't mean it doesn't exist) to union different measure units under one abstraction (like
and their descendants representing different measure system in every specific cases (metrical, imperial, astronomical, etc.). The only items that can combine base units are derived: velocity, acceleration, force, pressure and so on. Drafts I've created: interface Length {
companion object {
val ZERO: MetricLength = MetricLength.ZERO
}
operator fun plus(other: Length): Length = toMetric() + other.toMetric()
operator fun minus(other: Length): Length = toMetric() - other.toMetric()
operator fun div(other: Length): Double
operator fun times(scale: Int): Length
operator fun times(scale: Long): Length
operator fun times(scale: Double): Length
operator fun div(scale: Int): Length
operator fun div(scale: Long): Length
operator fun div(scale: Double): Length
fun abs(): Length
val isNegative: Boolean
val isPositive: Boolean
fun toMetric(): MetricLength
}
inline class MetricLength internal constructor(val value: Double) : Length, Comparable<MetricLength> {
companion object {
val ZERO: MetricLength = MetricLength(0.0)
val storageUnit = METRES
}
override fun compareTo(other: MetricLength): Int = value.compareTo(other.value)
override fun toMetric(): MetricLength = this
override operator fun plus(other: Length): MetricLength = MetricLength(value + other.toMetric().value)
override operator fun minus(other: Length): MetricLength = MetricLength(value - other.toMetric().value)
override operator fun times(scale: Int): MetricLength = MetricLength(value * scale)
override operator fun times(scale: Long): MetricLength = MetricLength(value * scale)
override operator fun times(scale: Double): MetricLength = MetricLength(value * scale)
override operator fun div(scale: Int): MetricLength = MetricLength(value / scale)
override operator fun div(scale: Long): MetricLength = MetricLength(value / scale)
override operator fun div(scale: Double): MetricLength = MetricLength(value / scale)
override operator fun div(other: Length): Double = this.value / other.toMetric().value
operator fun unaryMinus(): MetricLength = MetricLength(-value)
override val isNegative: Boolean get() = value < 0
override val isPositive: Boolean get() = value > 0
override fun abs(): MetricLength = if (isNegative) -this else this
fun toDouble(unit: MetricLengthUnit): Double = convertMetricUnit(value, storageUnit, unit)
fun toLong(unit: MetricLengthUnit): Long = toDouble(unit).toLong()
fun toInt(unit: MetricLengthUnit): Int = toDouble(unit).toInt()
val inMetres: Double get() = toDouble(METRES)
val inKilometres: Double get() = toDouble(KILOMETRES)
val inCentimetres: Double get() = toDouble(CENTIMETRES)
val inMillimetres: Double get() = toDouble(MILLIMETRES)
val inNanometres: Double get() = toDouble(NANOMETRES)
fun toLongMetres(): Long = toLong(METRES)
fun toLongKilometres(): Long = toLong(METRES)
fun toLongMillimetres(): Long = toLong(MILLIMETRES)
override fun toString(): String = when {
value < 1e-9 -> "${value * 1e12}${PICOMETRES.shortName}"
value < 1e-6 -> "${value * 1e9}${NANOMETRES.shortName}"
value < 1e-3 -> "${value * 1e6}${MICROMETRES.shortName}"
value < 1 -> "${value * 1e3}${MILLIMETRES.shortName}"
value < 1e3 -> "$value${METRES.shortName}"
else -> "${value * 1e-3}${KILOMETRES.shortName}"
}
}
enum class MetricLengthUnit(val power: Int) {
PICOMETRES(-12),
NANOMETRES(-9),
MICROMETRES(-6),
MILLIMETRES(-3),
CENTIMETRES(-2),
DECIMETRES(-1),
METRES(0),
KILOMETRES(3)
} But I clearly see, that this approach produces a lot of boilerplate. |
I aslo created init-extension and prototype of Speed (similar to Length/MetricLength): operator fun Int.times(length: MetricLength): MetricLength = length * this
fun Int.toMetricLength(unit: MetricLengthUnit): MetricLength =
MetricLength(convertMetricUnit(this.toDouble(), unit, MetricLength.storageUnit))
val Int.m get() = toMetricLength(METRES)
val Int.km get() = toMetricLength(KILOMETRES)
val Int.dm get() = toMetricLength(DECIMETRES)
val Int.cm get() = toMetricLength(CENTIMETRES)
val Int.mm get() = toMetricLength(MILLIMETRES)
val Int.um get() = toMetricLength(MICROMETRES)
val Int.nm get() = toMetricLength(NANOMETRES)
val Int.pm get() = toMetricLength(PICOMETRES) Length with Duration combiners: operator fun MetricLength.div(duration: Duration): MetricSpeed = MetricSpeed(inMetres / duration.inSeconds)
infix fun MetricLength.per(duration: Duration): MetricSpeed = this / duration
val Int.mPerS: MetricSpeed get() = MetricSpeed(this.toDouble())
val Int.kmPerS: MetricSpeed get() = MetricSpeed((this * 1000).toDouble())
val Int.kmPerH: MetricSpeed get() = MetricSpeed((this * 1000).toDouble() / 3600) After all, usage looks like: val length: MetricLength = 238.m
val speed1: MetricSpeed = 60.km per 1.hours
val speed2: MetricSpeed = 100.m / 9.8.seconds
val speed3: MetricSpeed = 8.kmPerS
val distance: MetricLength = speed3 * 1.hours But I still have not tried to introduce new measurment system to see, how extending easily is. |
I've just realised, that we can express some math operations as default in base interface through default measure system as interface has mapping to it and math will work for new systems out of the box (author still will have ability to override and add more of them). |
@altavir But I still has a question: why do you need this in math library? Isn't it more related to physic? |
@green-nick I do not see a motivation to make arithmetic operators members of the class. If we are sure that all values are just inline classes over Double (we can add another level of abstraction by providing, say, BigDecimal units for currency etc), the all we need is to expose this unsafe value and then add all operations as extensions on appropriate types. The only problem I see is that we can loose numeric precision when converting units too far away. I will need to write down correct recursive generics so it could be understood correctly. |
Our work is mostly about physics, so for us it makes sense to have it one place for now just to have a common place for all APIs. In future, it will be probably good idea to separate repositories. Also type safe units should look nice with geometry package which is now in development. |
Hello, did you consider |
@pedroteixeira Thanks for the reference. I did not even know about its existence. I do not think it is a route we want to take since it is rather heavyweight and we would probably want to use kotlin extensions and inline classes, but it makes sense to see different ideas and compare them. |
https://github.com/nacular/measured implements something very close to |
I'd be happy to explore this and see what can be done around non-boxing. |
A proposal here: nacular/measured#2 |
Add basic API for type-sage units via inline classes.
The text was updated successfully, but these errors were encountered: