Comments 34
Формулы и правда выглядят прикольно. А как с этим всем работает комплишен? Будет удобно нащелкать символ евро из предложенных вариантов на 5.
или проще учиться набирать юникод с клавиатуры?
Вот это очень круто конечно. Когда абстракция выстроена так, что сама не допускает логических ошибок.
С точки зрения системы типов (как бы Вы не утверждали, что то, что получилось у Вас, лучше), было бы корректнее добавлять для каждой из таких единиц измерения свой тип. И тогда все такие ошибки ловились бы не при "пробегающем" юнит-тесте, а еще при компиляции. Чем раньше мы находим ошибку, тем лучше
Спору нет, находить все ошибки при манипулировании с физическими и иными единицами уже на этапе компиляции было бы замечательно. Но увы, пока моя слабая фантазия не видит для этого другого способа кроме как создавать свой тип на каждую комбинацию размерностей - скорость, ускорение, плотность, и т.д. А их - безбрежное количество.
К тому же, это противоречит «логике физики», насколько я ее себе представляю.
Я размышляю сейчас над возможностью написания плагина на KSP для предпроцессинга кода с использованием KotUniL. Но пока, честно говоря, решения не нашёл. Да и по затратам это будет не так уж мало. А я тяну проект пока один в свободное от остальных забот время.
Кодогенерация может помочь. Описываются основные величины а дальше декартовым произведением перемножаются на возможные операции.
Не думаю, что это реально. Возьмём например скорость. Скорость движения - размерность m/s, скорость окраски забора - m2/s, скорость таяния льда - m3/s. Простое ускорение: m/s2, ускорение при окраске забора m/s2... и т.д. Это только простейшие комбинации двух размерностей. А много величин - комбинаций трёх, четырёх, пяти размерностей и т.д. Степени могут быть положительные и отрицательные. И главное - как упомнить названия получившихся классов? Уж тогда проще новый язык сделать. А мы хотим оставаться в Котлине.
Да, так и есть.
val operations = {умножение/деление}
var units = intialUnits
while(makeSense){
units = units*operations*units
}
Новый язык не одязательно делать, это уже реализовано в F#.
Вы имеете ввиду Units of mesure?
Насколько я могу судить, это менее удобно, чем KotUniL и беднее по функциональности. Или в F# существует ещё что-то на эту тему?
В JSR 385 для Java именно что пошли по пути выделения размерностей. У них получились Quantity<Volume>
, Unit<Length>
, и так далее. Разумеется, всех возможных комбинаций таким образом не покрыть, но множеству приложений этого делать и не придётся. При этом, стандартные размерности покрываются стандартным же образом, а при достаточно гибком дизайне библиотеки новые нужные для конкретно вашего приложения размерности добавить должно быть достаточно просто, и при этом не теряется типобезопасность.
Описаний хороших найти что-то не получается, но вот статья на Baeldung про общий вид работы с библиотекой.
UPD: Ну как я раньше не подумал пойти на главный сайт и потыкаться там?
Ссылки:
Я посмотрел примеры в рекомендованной Вами статье. Это мазохизм создателей и садизм по отношению к потенциальным пользователям. Чего стоят подобный пример конвертирования метров в километры, взятый мной из рекомендованной Вами статьи:
double distanceInMeters = 50.0;
UnitConverter metreToKilometre = METRE.getConverterTo(MetricPrefix.KILO(METRE));
double distanceInKilometers = metreToKilometre.convert(distanceInMeters );
Используя KotUniL вы это запишите так:
val d = 50.m
val x = d.km
Я понимаю, вы говорите про обнаружение ошибок на уровне компиляции, а я про наглядность и краткость кода.
В третьей статье этой серии я постараюсь показать, что любые формулы KotUniL проверяются ровным одним юнит-тестом.
Ничто на самом деле не мешает разработчикам JSR 385 сделать дополнительный метод
public Quantity<Q> convertTo(Unit<Q> quantity);
И тогда то, что вам не понравилось, будет выглядеть как-то так:
Quantity<Length> distance = getQuantity(2500d, METRE);
Quantity<Length> inKilometers = distance.convertTo(KILO(METRE));
А это, согласитесь, уже примерно то же, что и у вас, с поправкой на отсутствие в Java таких же возможностей по добавлению синтаксического сахара. И тайпчекается на ура.
Я же не предлагаю вам полностью повторить тот API. Там на мой взгляд многовато всяческих "мин" подложено — да хотя бы тот факт, что у них Unit
параметризуется через Quantity
, хотя по всем моим инстинктам они оба должны быть параметризованы через Dimension
.
UPD: Собственно, там уже есть такой метод, как я предложил: Quantity::to
Кроме длины в СИ есть ещё шесть других базовых размерностей. И 22 их распространнённых комбинации и сотни менее распространённых. И они могут сами далее комбинироваться.
Для каждой из этих комбинаций Вы предлагаете отдельный тип создавать?
А что такого?
- Acceleration
- Amount of Substance
- Angle
- Area
- Catalytic Activity
- Dimensionless
- Electric Capacitance
- Electric Charge
- Electric Conductance
- Electric Current
- Electric Inductance
- Electric Potential
- Electric Resistance
- Energy
- Force
- Frequency
- Illuminance
- Length
- Luminous Flux
- Luminous Intensity
- Magnetic Flux
- Magnetic Flux Density
- Mass
- Power
- Pressure
- Radiation Dose Absorbed
- Radiation Dose Effective
- Radioactivity
- Solid Angle
- Speed
- Temperature
- Time
- Volume
Безотносительно порой довольно странного содержания этого списка, покрыто достаточно много вариантов. И это в Java. В Kotlin можно через extension'ы доопределить, примерно так
fun Quantity<Length>.times(Quantity<Length>): Quantity<Area>
fun Quantity<Length>.div(Quantity<Time>): Quantity<Speed>
Приведённый Вами список не что иное, как соединение так называемых базовых и производных величин СИ. А ещё есть неофициальные, но признаваемые системой единицы, вроде гектара и литра. А ещё существуют национальные варианты типа кг. вместо kg. А ещё хорошо бы валюты так-же просто использовать. И пользователь свои единицы должен мочь определять. И - последний гвоздь в гроб этой идеи: очень часто нужны более сложные единицы, получаемые умножением или делением вышеперечисленных.
Вывод: это тупиковый путь.
А ещё есть неофициальные, но признаваемые системой единицы, вроде гектара и литра
Гектар это Area
, литр это Volume
. Они не строго дополняют список размерностей, они ему ортогональны, вообще говоря. Посмотрите в примерах и спеке у JSR, дюймы и метры там оба относятся к Unit<Length>
, точно так же, как кубометры и литры относятся к Unit<Volume>
.
А ещё существуют национальные варианты типа кг. вместо kg.
Зачем такое явно включать в программную модель? У java.time
(да и в принципе в библиотеках, где присутствует локализация) все такие вещи находятся на внешнем крае библиотеки, где-нибудь в Parser/Formatter
. У вас же в библиотеке нельзя написать val weight = 5.千克
? Вот и в любой другой это не пригодится. Но при большом желании, конечно, можно все эти имена методов сгенерировать.
А ещё хорошо бы валюты так-же просто использовать.
Это совершенно не то же самое, что к метру прибавить ярд, не получится у нас прибавить евро к доллару без доступа в Интернет. Monetary
— это в целом нормальный тип данных в нескольких виденных мной библиотеках, но там совершенно отдельная банка червей с подключением внешних сервисов конверсии (да хотя б через SPI) и правилами, разрешающими или запрещающими сложение количеств с разными валютами в конкретных местах приложений.
И пользователь свои единицы должен мочь определять.
А в чём проблема с этим в JSR-385?
И — последний гвоздь в гроб этой идеи: очень часто нужны более сложные единицы, получаемые умножением или делением вышеперечисленных.
Я ведь выше уже написал, как умножение и деление ввести в такую систему, и что в итоге даётся возможность полностью управлять типом результата (и вот там, кстати, и нужны юнит-тесты, а во всём остальном коде они будут ни к чему). В Kotlin это вообще куда проще сделать, чем в Java — у вас компаньоны есть, и операторы-по-конвенции, а в Java придётся это всё в статические методы запихать.
Вывод: это тупиковый путь.
Не согласен. Я лично только вижу, что вам очень не хочется нынешнюю концепцию библиотеки менять. Ну так никто, собственно, и не заставляет.
Но библиотека с альтернативным подходом сейчас есть, ей пользуется явно ненулевое количество приложений. Несмотря на моё несогласие с выбранным списком абстракций конкретно в JSR-385, её редизайн, на мой взгляд, мог бы стать очень мощным инструментом в перспективе, а в Kotlin с его количеством сахара и вовсе мёдом намазано попытаться.
Вы говорите сразу о как минимум двух темах: JSR 385 и возможных изменениях/улучшениях в KotUniL.
Я бы хотел эти темы разделить. Поэтому далее отвечаю на Ваш длинный пост по этим темам раздельно.
О JSR 365. Как я уже говорил, я не думаю, что из этого получится что-то интересное с точки зрения практики. Я попытался это вверху обосновать.
Я не собираюсь эту спецификацию улучшать или реализовывать. Посмотрим, что из этого выйдет. Дальше я касаться этой темы не хотел бы.
О KotUniL.
Я предложил эту или подобную библиотеку наконец-то вставить в стандартную поставку Котлина: https://youtrack.jetbrains.com/issue/KT-55556
Так что далее можно дискутировать уже конкретно в рамках этой заявки.
Например, если у Вас есть какие-то соображения по синтаксису - Вы можете переписать примеры из статей этой серии в Вашем синтаксисе. Мне очень интересно было бы понять Ваши предложения.
А пока несколько замечаний к Вашему комментарию.
Я считаю, что в Котлине необходимы библиотеки для работы с национальными единицами. Т.е. должно быть возможно написать на большинстве языков что-то вроде: val скорость = 60.км/час. Чтобы не перегружать поставку, это должно быть решено с помошью национальных вариантов библиотеки для популярных языков/культур.
Цели конвертировать валюты друг в друга не ставится. Это сложная функция, зависящая от времени, места, юрисдикций партнёров и т.д. Доллар и рубль это такие же единицы как секунда и киловатт или просто штука (посмотрите пример с Thing) в README.md в KotUniL. И манипулирование со всеми единицами происходит по одним и тем же правилам.
Я предложил эту или подобную библиотеку наконец-то вставить в стандартную поставку Котлина
Так как ваш вариант не предлагает проверки несоответствия типов значений на уровне компиляции, я считаю, что встраивание её в стандартную библиотеку серьёзно навредит возможному развитию этой библиотеки в будущем, потому что развитие этого API с добавлением проверок на этапе компиляции будет невозможно без нарушения обратной совместимости. Примерно так в своё время навредили стандартной библиотеке Java типы java.util.Date
и java.util.Calendar
, каждый со своими хвостами неправильных предположений о датах и времени, и кучам legacy-кода, который уже не переделать.
В виде обычной библиотеки я против вашего варианта API ничего не имею, тогда каждый найдёт что-то для себя, и будущее развитие никак не пострадает.
Я считаю, что в Котлине необходимы библиотеки для работы с национальными единицами. Т.е. должно быть возможно написать на большинстве языков что-то вроде: val скорость = 60.км/час.
Это не имеет смысла пока в Kotlin нет, например,
kt.Последовательность<out T>
.
Стандартная библиотека на то и стандартная, что представляет собой общий словарь, понятный любому программисту. Её перевод на другие языка дробит сообщество и создаёт анклавы программистов, неспособных читать код других программистов. Это явно противоречит интересам тех, кто разрабатывает и поддерживает стандартную библиотеку. Уже хотя бы потому, что вместо одной стандартной библиотеки появляется огромное множество региональных стандартных библиотек, несовместимых между собой, это увеличивает бремя поддержки самой библиотеки, и заставляет и иметь в штате специально обученных людей, которые будут принимать баги из региональных версий стандартной библиотеки, и переводить (или не переводить) их в термины глобальной стандартной библиотеки, если такая вообще имеется.
А что взамен — возможность чуть-чуть более знакомые имена методов применять?
Я не предлагаю расширять kotlin-stdlib, а добавить в число библиотек, поставляемых вместе с Котлином ещё одну. Она должна иметь статус как Ktor или Dokka. И иметь национальные варианты.
Это обещает расширить круг пользователей Котлина, включая студентов и школьников.
Школьникам и студентам совершенно ничто не помешает осваивать нелокализованную библиотеку. Программирование в широком смысле сводится просто к определённому набору символов, и 5.km/h
понимается и запоминается ими ничуть не хуже, чем 5.км/ч
.
В качестве бонуса, отсутствие локализации stdlib служит дополнительным вектором освоения иностранных языков.
Короче, я по-прежнему не понимаю, какой цели служит создание огромного множества библиотек с переведёнными на какой-нибудь другой язык названиями, и почему эти усилия по поддержке нельзя потратить вместо этого на, скажем, обеспечение типобезопасности в операциях по размерностям на этапе компиляции.
К счастью, мы подходим к фиксации различий наших точек зрения.
На каком языке учить школьников - это отдельный вопрос.
В мире есть много стран со своими единицами измерений типа дюймы, фунты и т.д. Есть системы стандартов физических единиц в отраслях и даже у отдельных концернов.
Так что Вы думаете - нужна одна библиотека, а я - нужно много.
В мире есть много стран со своими единицами измерений типа дюймы, фунты и т.д. Есть системы стандартов физических единиц в отраслях и даже у отдельных концернов.
Так я не пойму, в чём проблема-то здесь, по-прежнему?
Unit<Length> METER = ...
Unit<Length> INCH = ...
Unit<Length> FOOT = ...
Unit<Weight> GRAM = ...
Unit<Weight> POUND = ...
val length = (1.METER + 15.INCH).to(FOOT);
val myValue = 1.METER + 1.POUND; // compilation error
Определять можно вообще любую единицу измерения, в любой системе стандартов — для этого понадобится не более и не менее чем определить, какая система "главная" (почему бы и не SI?), и по какому правилу происходит преобразование из одного в другое. А можно и не определять заранее, какая из систем предпочитается вообще, тогда и правил никаких не нужно будет.
Библиотека — это же не только сами эти вот метры и дюймы, они должны быть поверх чего-то ещё общего, которое будет иметь сами правила определения единиц, и правила их операций между собой.
Условно говоря, это несколько библиотек:
kt.measures
kt.measures.si
(depends onkt.measures
)kt.measures.imperial
(depends onkt.measures
, optional dependency onkt.measures.si
).
Ну и всё, как-то так.
Да. Именно так. Примерно такое запланировано в https://github.com/vsirotin/si-units/issues
Спасибо Вам за упоминание JSR 385. Я использовал это как один из аргументов в предложении расширить Колин функциональностью из KotUniL.
Прикольно, круто. Но вот к double (как я понял) просто гвоздями прибито - это будет стрелять по ногам.
Это решение принято из прагматических соображений. А у Вас есть альтернативные предложения?
Много их. Посмотрите в сторону kmath
Хотя бы для денег Double не использовать.
Сложно сказать, как вам правильно будет: зависит от перспектив использования. Если нацелены на JVM, то BigDecimal неплохой вариант для денег. Если будут другие таргеты (JS, Native, KMM) то сходу не скажу. Но в них вы даже с спецсимволами в бэктиках помучаетесь.
Магия размерностей и магия Котлина. Часть первая: Введение в KotUniL