
Амперы нельзя складывать с вольтами. Сантиметры можно складывать с дюймами, но очень внимательно. Иначе получится как с космическим аппаратом стоимостью 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
