Comments 114
Всё уже придумано до нас: https://ru.wikipedia.org/wiki/Венгерская_нотация
И как она поможет? В контексте единиц времени.
m — это минута или месяц? Зачем запоминать совсем неочевидные сокращения? Экономить место, чернила в принтере листингов? Сколько их будет, сокращений?
А если не только время, но и длину? А вес? А денежные операции?
Никто не мешает придумать свои префиксы и их невозбранно использовать. Например, min, sec и так далее. Аналогично для длины, веса и прочего.
эти префиксы не проверяются на уровне типов. В результате они не спасают ни от опечаток, ни от недопонимания, ни от ошибок в документации
В исходной статье про типы - ни слова. Только про правила именования. Что касается системы типов и их проверки - это уже совершенно другая, отдельная проблема иного класса сложности.
P.S. Господи, ещё один адепт секты всеобщей типизации. Откуда вы только лезете в таких количествах?
Тоже сразу вспомнил ВН, а точнее мегастатью с критикой ВН, которую я читал много-много лет назад. Там автор в красках (хотя и не особо убедительно, на мой вкус) описывал, почему ВН это грязный и ненужный костыль - и в том числе на примере единиц измерения.
Вот именно, всё это уже давно известно, обсуждено и обсосано. А описанное в статье -- это венгерская нотация курильщика очередной велосипед.
Другой автор в красках описывал, как ВН может быть полезна, если её правильно готовить: https://www.joelonsoftware.com/2005/05/11/making-wrong-code-look-wrong/
Но не понимаю, как это можно применить к единицам измерения.
Речь о том, что число само по себе может быть интерпретировано как угодно и в этом его слабость. В предметной области скорее всего каждому числу соответствует то, что измеряют, и единица, в которой измеряют. В этом смысле даже "позиция элемента в списке" — это единица измерения. Как и секунда, как и валюта, как и пиксель, и автоинкрементный айдишник в базе. Будет нотация венгерской или какой — второстепенно. Главная мысль — выражайте явнее, какое число что измеряет и в какой единице. Просто путаница с секундами и миллисекундами это самый классический пример, в приведённой Вами статье есть ещё немало хороших примеров. Или вот прямо сегодня у меня коллега смешал в одну кучу переменные, которые измеряют сумму денег в единицах валют (грубо говоря, в евро) и в сотых долях этих валют (в евроцентах). В результате все шансы попасть на откат неправильно посчитанных операций. А можно было и не попадать, если в имени каждой переменной явно указать, это единицы или сотые доли. Вот и вся сказка.
Другой автор в красках описывал, как ВН может быть полезна, если её правильно готовить:
Пример не убедителен, при помощи типов это решается в разы надёжнее.
полностью поддерживаю
@gro там хороший пример, но надо понимать, что венгерская нотация в духе "Systems Hungarian" - это бред. Семантическая (которая "Apps Hungarian") - ее вообще язык венгерской нотацией не поворачивается назвать... А вполне себе просто нормальный способ именования переменных, но все равно он не поможет в случае, когда у Вас есть контейнер для времени, и куча возможностей засунуть туда разные единицы измерения. Примерно как в случае@ivegner Чем мне поможет наличие разных названий у переменных, если компилятор мне по рукам не даст при их приведении друг к другу? К сожалению, это может сделать только система типов.
@Keeper1 а почему префиксы должны быть одни, а не другие? Или скажем, у Вас будет префикс mGeorg и mKeeper - и как определить - какой из них для месяца (month), а какой для минуты? Или почему не делать это на другом языке, скажем, венгерском - perc для минуты? Или еще как-то? Я понимаю в системе типов, там хотя бы жестко ограничиваешь возможности перехода туда-сюда...
Цитирую себя же:
Никто не мешает придумать свои префиксы и их невозбранно использовать. Например, min, sec и так далее. Аналогично для длины, веса и прочего.
Аналогично для венгерского языка. Главное, чтобы 1С не получился.
надо же, плюсы не очень-то и отстают...
std::this_thread::wait_for(100ms);
delay(300.milliseconds)
К чему эти ненужные скобки и точка? Они несут лишнюю когнитивную нагрузку.
Давайте придумаем новый язык, например Мотлин, в котором можно будет писать так:
delay 300 milliseconds
(Ведь код чаще читают, чем компилируют и пишут!)
И да, в новом языке "забудем" про статические методы, накостылив их через companion object!
Как раз наоборот — до определенного предела знаки препинания позволяют быстрее понять, что именно за магия тут накручена. Например, если я вижу delay(300.milliseconds)
, я сразу могу определить, что:
- Вызывается метод
delay
с одним аргументом - Есть свойство
milliseconds
, скорее всего расширяемое, объявленное на типе целых чисел - Тип возвращаемого значения этого свойства совпадает с типом аргумента метода
delay
Если же я вижу delay 300 milliseconds
, то можно интерпретировать эту конструкцию совершенно по-разному:
- У метода
delay
два аргумента, первый — число, второй — некий алгебраический тип, перечисление единиц измерения времени, конструктором которого являетсяmilliseconds
- У метода
delay
один аргумент, аmilliseconds
— это постфиксный оператор (?), преобразующий число в специальный тип с диапазоном - У метода
delay
больше двух аргументов, и на этой строке мы только частично применили функцию, не вызвав ее — т.е. задержки не будет
Так что читаемость — это штука сугубо субъективная, и с количеством знаков препинания оно напрямую не коррелирует.
infix уже есть в котлине
В C# TimeSpan стараюсь использовать. Но вообще, по крайней мере в моей сфере, проблема глубже. Например, rotate(double angle) - градусы или радианы?
Можно использовать доменные типы, у которых единица измерения указана в имени типа, например rotate(Degrees angle)
. Вообще борьба с primitive obsession очень полезная штука (правда не во всех языках легко даётся).
Если были бы радианы, было бы подписано radians
Но в целом актуально.
В f# пошли ещё дальше в ввели единицы измерения.
https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/units-of-measure
Таким образом можно сделать так:
[<Measure>] type rad
let rotate (angle : double<rad>) = ...
И уже не получится случайно передать в функцию градусы.
Duration::from_millis(123).as_secs_f32(); //обычное число
Ну а если вдруг типы настолько умные, что длина * длина = площадь (и нет преобразования обратно в безразмерное число), то вы всегда можете периметр домножить на 1 единицу длины и получить то же числовое значение, но уже в виде площади.
sleep_seconds = 300
time.sleep(sleep_seconds)
Зачем писать два раза sleep? Вполне хватитsleep(secs)
. Излишняя подробность тоже утомляет.
Даже в Python, где
time.sleep
определяется с одним аргументом по имениsecs
, мы не можем вызватьsleep(secs=300)
из-за особенностей реализации.
Можем: sleep(secs:=300)
.
О да, вспоминаю прекрасную функцию в нашем проекте, которая имела 2 оверлоада
- int в мс
- float в сек
Timer.Show(1, () => {});
и
Timer.Show(1f, () => {});
Различались по работе всего в 1000 раз.
Как это прошло код-ревью?
Timer.Show(1, () => {});
и
Timer.Show(1f, () => {});
Особенно весело наверное выглядело бы в коде, например
var timeToShow = 100; // да да, название переменной не соответствует теме статьи ))
...
// куча
// некого
// кода
...
Timer.Show(t, () => {});
Используйте типы.
sleep(Seconds(10));
sleep(10.ms()); // в Rust можно так
Намного лучше префиксов, как по мне. Нельзя просто так взять и int засунуть, нужно сначала его в Seconds преобразовать. Сразу понятно становится, с какой единецей измерения ты работаешь
А как потом воевать с классами `com.google.Seconds`, `com.apple.util.Seconds`, `com.netflix.time.Seconds`, `org.someshit.Seconds`?
Не тип, а type class, и, возможно, не в языке, а в широко распространённой библиотеке. Пример из реальной жизни - сериализация: serde предоставляет тайпклассы (трейты, в терминологии Rust) Serialize и Serializer, тип данных реализовывает первый, формат сериализации (json, bincode, etc.) - второй, и serde обеспечивает их склейку. Так же, технически, может быть и тут - тайпкласс для секунд, который может быть реализован кем угодно, хоть гуглом, хоть эпплом, хоть нами самими (вероятнее, правда, это всё-таки будет тайпкласс Duration, без явной привязки к конкретной единице измерения, но с возможностью конвертироваться в них).
В случае конкретно Rust, впрочем, это не нужно, именно потому что есть https://doc.rust-lang.org/stable/core/time/struct.Duration.html (не в языке, формально, но в стандартной библиотеке), так что воевать особо ни с чем не нужно. Ну, разве что учитывать, что стандартный тип всё-таки ограничен и иногда лучше использовать "стандарт де-факто" в виде https://docs.rs/chrono/latest/chrono/struct.Duration.html (но опять-таки, это именно "стандарт де-факто", а не "один из многих").
Я имею в виду, что функция, принимающая impl Duration, сама может дёрнуть на нём seconds(), milliseconds() или что там ей нужно, чтобы получить сырое число для передачи в низкоуровневый метод (системный вызов или FFI). Либо продолжит работать с impl Duration как с чёрным ящиком, который можно прибавлять к impl Instant для получения другого impl Instant и сравнения, например, с Instant::now.
это ведь как раз подмножество типов, связанных со временем, но в ядре языка.
Не совсем понимаю, что за "подмножество". Есть один тайпкласс, который предоставляет несколько возможностей получить условный Integer, в зависимости от того, что нужно нижележащему слою. О каких типах тут речь?
Спать рано или поздно придётся в конкретных единицах времени, увы.
А как, по Вашему опыту, может быть реализовано это "спать"? Я вижу два варианта: либо упомянутый syscall/FFI (которым, с точки зрения типов, нужен именно "условный Integer"), либо "периодически просыпаться, проверять, не пора ли продолжать, если не пора - спать дальше", который прекрасно реализуется по принципу "запомнили impl Instant в начале сна и impl Duration на его продолжительность, при пробуждении проверяем, что start + duration < now".
Ну, изначально-то разговор был о том, как в Rust справляются с ситуациями, когда есть пачка разных реализаций одного и того же функционала (т.е. когда уже есть - ну, или предвидится - наличие этого самого "подмножества типов"). Ответ именно в этом - стандартный (де-юре или де-факто) тайпкласс, который реализуют все, кто хочет вписаться в экосистему.
Значит "реализуют интерфейс, характерный для секунд"
Трейты можно реализовывать вне объявления типа, поэтому можно дописывать типы под нужды библиотек.
Соответственно через трейты можно теоретически объеденить несколько библиотек, считающих время, просто при вызовах типы будут прозрачно конвертироваться или реализовывать нужные интерфейсы поверх.
Отлично. А чей трейт использовать -- com.google.TraitSeconds, com.apple.TraitSeconds, org.someshit.TraitSeconds?
rs.time.TraitSeconds, который будут реализовывать все, кто хочет, чтобы их использовали не только в рамках их собственной узкой ниши - и google, и apple, и someshit.
Значит, для начала надо затащить rs.time.TraitSeconds в стандартную библиотеку. А потом еще градусы цельсия, килограммы, радианы, ньютоны, амперы, года, световые года и прочее. Кто этим будет заниматься?
time.Duration
:time.Sleep(time.Second * 30)
time.Sleep(time.Millisecond * 300)
time.Sleep(300) // nanoseconds
Да, это великая штука. Переваривает все возможные размерности, что дико упрощает работу, особенно со всякими справочниками. Тут миллиметры, там метры, здесь мегапаскали, тут килограммы на квадратный метр, а вот у этого американского материала фунты на квадратный дюйм - отлично, не забудь это указать, и получишь нужный результат без дополнительных усилий по конвертации. Заодно и дополнительный контроль, потому что в ответе число тоже будет с размерностью, и если она не та - это сразу видно.
time.Duration
- это недоразумение, а не типизация.
Вот это вот компилируется:
time.Sleep(time.Second * time.Second)
Duration
это всего лишь int64
:// A Duration represents the elapsed time between two instants
// as an int64 nanosecond count. The representation limits the
// largest representable duration to approximately 290 years.
type Duration int64
Ваш пример конечно компилируется, но компилятор можно/нужно дополнять линтерами. Например,
golangci-lint
покажет сообщение от линтера durationcheck
:Multiplication of durations: `time.Second * time.Second` (durationcheck)
time.Sleep(time.Second * time.Second)
Внутри
Duration
это всего лишьint64
То что есть zero-cost абстракции - это совершенно нормально. Ненормально что эта конкретная абстракция дырява чуть менее чем полностью.
Вот так кстати тоже можно:time.Sleep(time.Second + 42)
Складываем яблоки с апельсинами, а чо бы и нет.
durationcheck
наверно можно доработать так, чтобы он ловил такое использование констант без явного указания «единицы времени», т.е вместоvar duration time.Duration = 100
time.Sleep(300)
time.Sleep(time.Second + 42)
надо было бы явно писать
var duration = time.Nanosecond * 100
time.Sleep(time.Nanosecond * 300)
time.Sleep(time.Second + time.Nanosecond*42)
В данном случае 42 — это не апельсины, а 42 наносекунды (те же яблоки).
Ну то есть таки яблоки с апельсинами (секунды с наносекундами). Да, и то и другое - фрукты единицы времени, но толку, если оно читается не так, как работает?
*
ошибочно написан +
.Аналогичный тип для «массы» выглядел бы примерно так:
type Mass int64
const (
Milligram Mass = 1
Gram = 1000 * Milligram
Kilogram = 1000 * Gram
)
var mass = Gram + 42
Но time.Sleep
всё ещё можно скормить голое число. Выглядит не очень надёжно.
С инженерной колокольни я сказал бы - знай язык, на котором работаешь. Например, если я читаю данные с чертежа и вношу их в скрипт на языке прочностного пакета ANSYS - то я знаю, что на чертеже размеры будут в миллиметрах, а пакет прочитает переменную из скрипта как метры. И соответственно не забуду поделить число на тысячу - либо в уме, либо добавив в скрипт эту операцию.
Почему у программистов это не так? Языков много, и для всех помнить не получается? Или есть другие причины?
Thread.sleep(3000); //Ms
не благодарите
Кстати, вспомнилось OpenXml. Там же дичь с единицами измерений шрифтов, полей, отступов. Где что сейчас не вспомню, но там и миллиметры, и дюймы, и 1/1440 дюйма, и проценты, и ещё невесть что.
еще можно так
let sleepSeconds = sleep
sleepSeconds(5)
Вообще, за концепцию «хороший код должен читаться без комментариев, поэтому комментарии не нужны» надо бить. Оно, конечно, на каком-то самом низком уровне намного проще и приятнее разбираться в коде типа
for NumberOfRecords = 0 to MaxRecordNumber...
Немного проще, чем в
for i=0 to a
Но развёрнутый комментарий позволяет написать не только «что мы делаем», но и «зачем мы это делаем».
Мне доводилось писать физические расчёты на Питоне, используя pint для контроля размерности. Проблемы возникали в нескольких местах:
загрузка/сохранение конфигураций. Пришлось городить костыль для хранения единиц измерения рядом со значением.
Любой алгоритм, требущий сторонней библиотеки, приводит к необходимости сначала оторвать единицы измерения от исходных данных, а потом вручную добавить нужные единицы к результатам.
То и дело требуется явно проверить размерность, или явно привести к заданной размерности. Например, pint любит разбирать ньютоны (Н) на составные части (кг*м / с^2), что неудобно при выводе.
Пока нет поддержки типизации, т.е. нельзя сделать type hint с указанием размерности.
Но в целом не скажу, что pint не стоил затраченных усилий.
Глядя на запись "ждать 300 чего-то" - пусть даже там как-то будет указано "300 секунд" или "300 минут", у меня прежде всего возникнет вопрос - а почему именно 300?
Мне кажется, в подобной ситуации напрашивается введение константы (в название которой можно указать sec300 или ms300) и использование в sleep(ms300) именно её
Это все последствия слабой системы типов.
Пример, как это сделано правильно: в Golang есть тип `time.Duration`, в который можно передать данные в любом виде - миллисекунды, минуты, часы или что хотите. В итоге вопросов вида "что лежит в переменной" не возникает - там лежит длительность с абсолютной размерностью, которую при необходимости (в случае вывода в консоль, например) можно преобразовать к любой нужной величине - секундам, минутам и т.п.
что на счет js? и typescript?
гошный линтер, который может помочь. https://github.com/charithe/durationcheck
Поэтому у нас на кодревью принято заворачивать подобные конструкции и использовать спаны (C#):
await Task.Delay(TimeSpan.FromSeconds(3));
для инженерных задач вполне себе опробованный способ
Всегда так делаю. В текущем проекте, например, есть такие переменные: weightInKg, lengthInCm, closeSailsInXHours, closeSailsInXMinutes
ага, а теперь обеспечь атомарность их изменения. Потому что изменив closeSailsInXHours, но забыв изменить closeSailsInXMinutes (если это полное время путешествия, просто в разных единицах) - можно где-то в программе получить типичный race condition и соответствующий мусор в данных...
В моем случае closeSailsInXHours и closeSailsInXMinutes могут использоваться вместе, если нужно "закрыть" продажи на рейс за 1.5 часа, например.
то есть это доли одной величины - в этом случае это будет closeSailsInXHours=1 и closeSailsInXMinutes=30, но это точно так же означает, что нужно быть с этими величинами аккуратными, чтобы учитывать переносы (как только closeSailsInXMinutes>60 - делать перенос 1 часа в closeSailsInXHours и наоборот), и то, что их нужно смотреть в строго определенные моменты, чтобы данные были консистентными. Согласны? Как это обеспечивает? Или у Вас код исключительно однопоточный?
Увидев было тег Python и прочитав начало статьи, я обрадовался, подумав, что модуль astropy.units доехал до стандартной библиотеки, а с ним можно проворачивать вещи в духе
from astropy import units as u
def my_sleep(s: u.second): pass
my_sleep(42 * u.second)
Но увы.
Добавляйте единицы измерения в имена