KotUniL = SI Units + Kotlin. Part One: Introduction to KotUniL
Amperes cannot be added to volts. Centimetres can be added to inches, but very carefully. Otherwise, it will be like with the $125 million Mars Climate Orbiter spacecraft, which successfully reached Mars, but crashed miserably on its surface.
It crashed because its software developers did not consider the difference in physical units used in different parts of the system. For the same reason, before and after this expensive accident, spacecraft and aircraft have exploded and fallen, ships have sunk, and people have died.
These disasters and deaths could have been avoided if the on-board and system software programmers had used specialized libraries like KotUniL, which I want to talk about in this series of articles.
The first (this) article is actually about the library, its features and simple rules of use. The articles in the series touch on topics that may be useful and interesting to all programmers, regardless of the language they use, although they may be most useful to Kotlin programmers.
These are the articles of this series:
KotUniL = SI Units + Kotlin. Part One: Introduction to KotUniL (this)
KotUniL = SI Units + Kotlin. Part Two: Advanced Features
KotUniL = SI Units + Kotlin. Part Three: When only one unit test is enough
How it all began
In my project I came across the need to work with formulas using physical quantities of the SI system (see https://en.wikipedia.org/wiki/International_System_of_Units), which on the one hand must be programmed in Kotlin, but on the other hand must be understandable to physicists.
It is my deep conviction that libraries for working with physical quantities should, along with libraries for working with files, Internet protocols, etc., belong to the standard libraries supplied by the developers of the language. However, this is not the case. Which leads to the fact that many firms for a lot of money develop them themselves.
Unfortunately, Kotlin also comes without such a library. On GitHub you can find several contenders for this role. Having looked through them, I haven’t found any library that meets my requirements. (I hope I didn’t miss one in my search). Frustrated by this fact, I decided to write my own library.
By doing so I dived into the magic of physical dimensions and emerged in the magic of type algebra.
Further in this article I’ll briefly talk about the results.
KotUniL
KotUniL (Kotlin Units Library) is a library of Kotlin functions and objects that generally meet the following requirements:
- It covers all units of SI base, derived, accepted units and SI- Prefixes (see https://en.wikipedia.org/wiki/International_System_of_Units) like meter, second etc. as well as some other common units like currencies, percentages etc.
- It allows various formulas to be written in Kotlin in a way maximally similar to the way formulas are written in physics and economics.
- It allows analyzing dimensions of results of applications of complicated formulas.
- It allows to detect most of the typical errors when working with SI units already at the compilation stage. Errors in incorrect use of physical units in complex formulas are detected by first unit test.
- It is pure library (no plug-in, no parser etc.), it has no dependencies
Not very clear? Then let’s look at how the library works on a number of examples, starting with the simplest. (I’m not going to get fancy and give here some examples from the library’s documentation on GitHub https://github.com/vsirotin/si-units ).
Simple physical formulas
In Eva’s case, a glass broke in her aquarium and water flowed on floor. In aquarium before breakage was 32 liters of water. Eva’s room is 4 m. wide and 4.3 long. How high in mm. is water now in Eva’s room with assumption that it stayed there and did not flow away?
The solution in Kotlin can be written in one line. For didactic reasons like introduce two auxiliary variables s and h.
val s = 4.m * 4.3.m
val h = 32.l/s
print("Water height is ${h.mm} mm.")
Let’s take a closer look. The area of a room is measured in square meters. The variable s, as a result of multiplying meters by meters, has implicitly acquired this dimensionality. A liter is a thousandth of a cubic meter. Cubic meters divided by square meters gives just meters (variable h). But we want to translate them into millimetres, which is what we do when printing.
It’s more than a type safety
“What does a spacecraft that crashed on the surface of Mars have to do with this?” — some readers may ask.
The point is that if we specify the knowledge of variables in our calculations not only in numbers, but also in physical (and not only) dimensions. Errors when trying to manipulate wrong dimensions will be revealed either already at the compilation stage or at the first unit test “running” on the wrong formula:
//val x = 1.m + 2 compilation error
//val y = 20.l/(4.m * 5.m) + 14 compilation error
//Complex errors will be found in runtime:
val exception = assertFailsWith<IllegalArgumentException>(
block = { 1.m + 2.s }
)
assertTrue(exception.message!!.startsWith(COMPATIBILITY_ERR_PREFIX))
I want to emphasize this peculiarity of the library: if your formula is incorrect, then regardless of the values of physical quantities used, any unit test that “ran” on it will show it to be incorrect. I will try to show why this is so in the next article in this series.
For now, let’s focus on the fact that the described library feature is more than a classic type safety. We are talking here not only about the correctness of the units themselves, but also the results calculated using arithmetic formulas of arbitrary complexity.
Comparing complex objects
The library allows you not only to add, subtract, multiply, divide, and raise physical and other units. It can correctly compare them. And not only the original units, but also their derivatives obtained with the above operations.
As in the case of addition and subtraction, only objects of the same type can be compared:
assertTrue(5.m > 4.1.m)
assertTrue(20.2*m3 > 4.2*m3)
assertTrue(2.2*kg*m/s < 4.2*kg*m/s)
If the library tries to compare objects of different types, it will throw IllegalArgumentException
val v1 = 2.4.m
val v2 = 2.4.s
val exception = assertFailsWith<IllegalArgumentException>(
block = { v1 >= v2 }
)
assertTrue(exception.message!!.startsWith(COMPATIBILITY_ERR_PREFIX))
or
val v1 = 2.4.m*kg/s
val v2 = 2.4.s*m3/μV
val exception = assertFailsWith<IllegalArgumentException>(
block = { v1 >= v2 }
)
assertTrue(exception.message!!.startsWith(COMPATIBILITY_ERR_PREFIX))
In case you’re wondering what μV means, it’s microvolts. But we’ll talk about them and other prefix expressions within the SI system and KotUniL in the next article of the series.
And how to use it?
As mentioned above, KotUniL is a Kotlin library without any external dependencies. So it’s very easy to use it to your Kotlin project. In the case of gradle/KTS, this is done by adding lines to your build.gradle.kts:
repositories {
mavenCentral()
}
dependencies {
implementation("eu.sirotin.kotunil:all:3.0.0")
}
Similar dependencies you need to add to the pom file in case you use Maven:
<dependency>
<groupId>eu.sirotin.kotunil</groupId>
<artifactId>all</artifactId>
<version>3.0.0</version>
</dependency>
Please note. The development of KotUniL is ongoing. Please check if a newer version is currently available.
You can find the source code of the library on GitHub: https://github.com/vsirotin/si-units
If you add an asterisk to the project, the author won’t be offended :-)
That’s where we end our introduction to the main features of the library. In the next article of this series we will get acquainted with its more advanced features.
Illustration: The Mars Climate Orbiter spacecraft crashed due to the programmers’ fault for not working correctly with physical dimensions. Source: Wikipedia