Магия размерностей и магия Котлина. Часть первая: Введение в KotUniL

Mars Climate OrbiterMars Climate Orbiter

Амперы нельзя складывать с вольтами. Сантиметры можно складывать с дюймами, но очень внимательно. Иначе получится как с космическим аппаратом стоимостью 125 миллионов долларов Mars Climate Orbiter, который успешно долетел до Марса, но бездарно разбился о его  поверхность. 

Он разбился, поскольку разработчики его программного обеспечения не учли разницу используемых в разных частях системы физических единиц. По этой же самой причине до и после этой дорогой аварии взрывались и падали космические и летательные аппараты, тонули корабли и погибали люди.

Этих катастроф и смертей можно было бы избежать, если бы программисты бортового и системного ПО использовали в своей работе специализированные библиотеки типа KotUniL, о которой я хочу рассказать в этой серии статей. 

Первая (эта) статья собственно о библиотеке, её возможностях и нехитрых правилах использования. Другие статьи этой серии затрагивают темы, которые могут оказаться полезными и интересными всем программистам, вне зависимости от используемого ими языка, хотя «котлинцам» они могут пригодиться больше других.

Вот полный список статей серии:  

  1. Магия размерностей и магия Котлина. Часть первая: Введение в KotUniL

  2. Магия размерностей и магия Котлина. Часть вторая: Продвинутые возможности  KotUniL (будет скоро опубликована)

  3. Магия размерностей и магия Котлина. Часть третья: Смешение магий (будет скоро опубликована)

Как всё начиналось

В своём проекте я наткнулся на необходимость работы с формулами, использующими физические величины системы СИ, которые с одной стороны должны быть запрограммированы на Котлине, но с другой стороны должны быть понятны физикам.  

По моему глубокому убеждению, библиотеки для работы с физическими величинами должны, наряду с библиотеками для работы с файлами, интернет-протоколами и т.п. принадлежать к числу стандартных библиотек, поставляемых разработчиками языка. Тем не менее, это не так. Что приводит к тому, что фирмы — «акулы энтерпрайза» за большие деньги сами их разрабатывают. 

К сожалению, и мой любимый Котлин также поставляется без такой библиотеки. Свято место не бывает, и на GitHub можно найти несколько претендентов на эту роль. Поизучав их, я не нашёл ни одной библиотеки, удовлетворяющей моим требованиям. (Надеюсь, в своих поисках я не пропустил такую). Огорчённый этим обстоятельством, я решил написать собственную библиотеку.

Тем самым я нырнул в магию физических размерностей, а вынырнул в магии алгебры типов.

Далее в этой статье я вкратце рассажу о достигнутых результатах. 

KotUniL

KotUniL (Kotlin Units Library) — это библиотека функций и объектов Kotlin, которые в целом отвечают следующим требованиям:

  1. Охватывает все базовые единицы СИ, такие как метр, секунда и т.д., а также некоторые другие распространенные нефизические единицы, такие как валюты, процент и т.д.

  2. Умеет аккуратно работать со всеми префиксами системы СИ — микро, нано, кило и т.д.

  3. Позволяет записывать различные формулы на языке Kotlin способом, максимально похожим на то, как формулы записываются в физике и экономике.

  4. Позволяет анализировать размерность результатов применения сложных формул.

  5. Позволяет обнаружить большинство типичных ошибок при работе с единицами СИ уже на этапе компиляции. Ошибки некорректного использования физических единиц в сложных формулах выявляются во время выполнения, но могут быть легко обнаружены при обычном юнит-тестировании.

  6. Это чистая библиотека (без плагина, без парсера и т.д.), не имеющая никаких зависимостей от сторонних библиотек.

Не очень понятно? Тогда давайте рассмотрим, как работает библиотека на ряде примеров, начиная с простейшего. (Не стану мудрить и приведу здесь несколько примеров из документации библиотеки на 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(
  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(
   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(
   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:

    

        eu.sirotin.kotunil

        kotunil

        1.0.1

    

Ну, а исходные коды библиотеки вы найдёте на GitHub: https://github.com/vsirotin/si-units 

Если вы при этом добавите проекту звёздочку, автор в обиде не будет :-)

На этом мы закончим знакомство с основными возможностями библиотеки. В следующей статье этой серии мы познакомимся с её «продвинутыми» фичам.


Иллюстрация: Космический аппарат  Mars Climate Orbiter разбившийся по вине программистов, неправильно работавших с физическими размерностями. Источник: Wikipedia

© Habrahabr.ru