Амперы нельзя складывать с вольтами. Сантиметры можно складывать с дюймами, но очень внимательно. Иначе получится как с космическим аппаратом стоимостью 125 миллионов долларов Mars Climate Orbiter, который успешно долетел до Марса, но бездарно разбился о его поверхность.
Он разбился, поскольку разработчики его программного обеспечения не учли разницу используемых в разных частях системы физических единиц. По этой же самой причине до и после этой дорогой аварии взрывались и падали космические и летательные аппараты, тонули корабли и погибали люди.
Этих катастроф и смертей можно было бы избежать, если бы программисты бортового и системного ПО использовали в своей работе специализированные библиотеки типа KotUniL, о которой я хочу рассказать в этой серии статей.
Первая (эта) статья собственно о библиотеке, её возможностях и нехитрых правилах использования. Другие статьи этой серии затрагивают темы, которые могут оказаться полезными и интересными всем программистам, вне зависимости от используемого ими языка, хотя “котлинцам” они могут пригодиться больше других.
Вот полный список статей серии:
Магия размерностей и магия Котлина. Часть первая: Введение в KotUniL
Магия размерностей и магия Котлина. Часть вторая: Продвинутые возможности KotUniL
Магия размерностей и магия Котлина. Часть третья: Смешение магий
Как всё начиналось
В своём проекте я наткнулся на необходимость работы с формулами, использующими физические величины системы СИ, которые с одной стороны должны быть запрограммированы на Котлине, но с другой стороны должны быть понятны физикам.
По моему глубокому убеждению, библиотеки для работы с физическими величинами должны, наряду с библиотеками для работы с файлами, интернет-протоколами и т.п. принадлежать к числу стандартных библиотек, поставляемых разработчиками языка. Тем не менее, это не так. Что приводит к тому, что фирмы - “акулы энтерпрайза” за большие деньги сами их разрабатывают.
К сожалению, и мой любимый Котлин также поставляется без такой библиотеки. Свято место не бывает, и на GitHub можно найти несколько претендентов на эту роль. Поизучав их, я не нашёл ни одной библиотеки, удовлетворяющей моим требованиям. (Надеюсь, в своих поисках я не пропустил такую). Огорчённый этим обстоятельством, я решил написать собственную библиотеку.
Тем самым я нырнул в магию физических размерностей, а вынырнул в магии алгебры типов.
Далее в этой статье я вкратце рассажу о достигнутых результатах.
KotUniL
KotUniL (Kotlin Units Library) - это библиотека функций и объектов Kotlin, которые в целом отвечают следующим требованиям:
Охватывает все базовые единицы СИ, такие как метр, секунда и т.д., а также некоторые другие распространенные нефизические единицы, такие как валюты, процент и т.д.
Умеет аккуратно работать со всеми префиксами системы СИ - микро, нано, кило и т.д.
Позволяет записывать различные формулы на языке Kotlin способом, максимально похожим на то, как формулы записываются в физике и экономике.
Позволяет анализировать размерность результатов применения сложных формул.
Позволяет обнаружить большинство типичных ошибок при работе с единицами СИ уже на этапе компиляции. Ошибки некорректного использования физических единиц в сложных формулах выявляются во время выполнения, но могут быть легко обнаружены при обычном юнит-тестировании.
Это чистая библиотека (без плагина, без парсера и т.д.), не имеющая никаких зависимостей от сторонних библиотек.
Не очень понятно? Тогда давайте рассмотрим, как работает библиотека на ряде примеров, начиная с простейшего. (Не стану мудрить и приведу здесь несколько примеров из документации библиотеки на GitHub).
Маша мыла... аквариум
Рассмотрим первый пример.
Маша протирала снаружи стекло аквариума, задела стоявшую рядом вазу, в результате чего стекло аквариума разбилось и вода вытекла на пол. В аквариуме до этой неприятности было 32 литра воды. Комната Маши имеет длину 4 метра и ширину 4,3 метра. На какой высоте в мм. находится сейчас вода в комнате, при условии, что она осталась там и не вытекла?
Решение на языке Kotlin/KotUniL может быть записано одной строкой. В дидактических целях введем две вспомогательные переменные s и h для площади комнаты и уровня воды в комнате.
val s = 4.m * 4.3.m
val h = 32.l/s
print(«Высота воды в комнате ${h.mm} mm."
Приглядимся повнимательнее. Площадь комнаты измеряется в квадратных метрах. Переменная s в результате перемножения метров на метры получила неявным образом эту размерность. А литр - это тысячная часть кубического метра. Кубические метры, поделённые на квадратные метры дают в итоге просто метры (переменная h). Но нам хочется перевести их миллиметры, что мы и делаем при печати.
Это больше чем type safety
“А причём тут разбившийся о поверхность Марса космический аппарат?” - возможно спросит кто-то из читателей.
Всё дело в том, что если мы будем задавать знания переменных наших расчетах не просто числами, а указывать при этом физические (и не только) размерности, ошибки при попытке манипулировать неправильными размерностями выявятся либо уже на этапе компиляции либо при первом же юнит-тесте, “пробегающим” по неправильной формуле:
//val x = 1.m + 2 ошибка компиляции
//val y = 20.l/(4.m * 5.m) + 14 ошибка компиляции
//Более заковыристые ошибки выявляются в runtime:
val exception = assertFailsWith<IllegalArgumentException>(
block = { 1.m + 2.s }
)
assertTrue(exception.message!!.startsWith(COMPATIBILITY_ERR_PREFIX))
Я хочу особенно подчеркнуть эту особенность библиотеки: если ваша формула некорректна, то вне зависимости от используемых значений физических величин любой “пробежавший” по ней юнит-тест покажет её некорректность. Почему это так, я постараюсь показать в следующей статье этой серии.
Пока же обратим внимание на то, что описанная фича библиотеки это больше чем классический type safety. Речь здесь идёт не только о корректности самих единиц, но и результатов, вычисленных с помощью арифметических формул произвольной сложности.
Сравнение сложных объектов
Библиотека позволяет не только складывать, вычитать, умножать, делить и возводить в степень физические и иные единицы. Она умеет их корректно сравнивать. Причём не только исходные единицы, но и производные от них, полученные с помощью перечисленных выше операций.
Как и в случае со сложением и вычитанием, сравнивать можно только объекты одного типа:
assertTrue(5.m > 4.1.m)
assertTrue(20.2*m3 > 4.2*m3)
assertTrue(2.2*kg*m/s < 4.2*kg*m/s)
При попытке сравнивать объекты разных типов библиотекой будет выброшено 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))
или:
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))
Если вас заинтересовало, что означает μV - это микровольты. Но про них и прочие префиксные выражения в рамках системы СИ и KotUniL мы поговорим в следующей статье серии.
Анализ размерностей
Анализ размерностей очень интересная область физики, позволяющая быстро строить и проверять гипотезы. Вдаваться в эту тему не входит в мои планы, однако интересующимся могу порекомендовать вот эту и эту статьи.
При работе с физическими и другими размерностями можно “нагородить” настолько сложные формулы, что хорошо было бы узнать, какая размерность в конце-концов у нас получилась. Это позволяют сделать две функции библиотеки.
В системе СИ для каждой физической единицы задано его имя (например метр, или m) и категория (в случае метра - это длина, или L).
Функция unitSymbols() показывает символы размеренности и их степени:
val s = 4.m * 5.m
assertEquals("m2", s.unitSymbols())
val x = 20.l
assertEquals("m3", x.unitSymbols())
val h = x/s
assertEquals("m", h.unitSymbols())
val y = 1.2.s
assertEquals("s", y.unitSymbols())
val z = x/y
assertEquals("m3/s", z.unitSymbols())
А функция categorySymbols() - примерно тоже самое для категорий и несколько в другом виде::
val s = 4.m * 5.m
assertEquals("L2", s.categorySymbols())
val x = 20.l
assertEquals("L3", x.categorySymbols())
val h = x/s
assertEquals("L", h.categorySymbols())
val y = 1.2.s
assertEquals("T", y.categorySymbols())
val z = x/y
assertEquals("L3T-1", z.categorySymbols())
А как использовать?
Как уже говорилось выше, KotUniL - это библиотека Котлина без каких-либо внешних зависимостей. Поэтому её очень просто подключить к вашему Котлин-проекту. В случае gtadle/KTS это делается добавлением в ваш build.gradle.kts строк:
repositories {
mavenCentral()
}
dependencies {
implementation("eu.sirotin.kotunil:kotunil:1.0.1")
}
Аналогичные зависимости вам нужно добавить в pom-файл в случае использования Maven:
<dependency>
<groupId>eu.sirotin.kotunil</groupId>
<artifactId>kotunil</artifactId>
<version>1.0.1</version>
</dependency>
Ну а исходные коды библиотеки вы найдёте на GitHub: https://github.com/vsirotin/si-units
Если вы при этом добавите проекту звёздочку, автор в обиде не будет :-)
На этом мы закончим знакомство с основными возможностями библиотеки. В следующей статье этой серии мы познакомимся с её “продвинутыми” фичам.
Иллюстрация: Космический аппарат Mars Climate Orbiter разбившийся по вине программистов, неправильно работавших с физическими размерностями. Источник: Wikipedia