Есть одна ловушка читаемости кода, которой легко избежать, если вы о ней знаете; тем не менее она встречается постоянно: это отсутствующие единицы измерения. Рассмотрим три фрагмента кода на Python, Java и Haskell:
time.sleep(300)
Thread.sleep(300)
threadDelay 300
Сколько «спят» эти программы? Программа на Python выполняет задержку на пять минут, программа на Java — на 0,3 секунды, а программа на Haskell — на 0,3 миллисекунды.
Как это можно понять из кода? А никак. Вам просто нужно знать, что аргументом
time.sleep
являются секунды, а threadDelay
— микросекунды. Если вы часто ищете эту информацию, то рано или поздно её запомните, но как сохранить читаемость кода для людей, никогда не встречавшихся с time.sleep
?Вариант 1: вставить единицу измерения в имя
Вместо этого:
def frobnicate(timeout: int) -> None:
...
frobnicate(300)
сделаем вот так:
def frobnicate(*, timeout_seconds: int) -> None:
# The * forces the caller to use named arguments
# for all arguments after the *.
...
frobnicate(timeout_seconds=300)
В первом случае мы даже не можем сказать в месте вызова, что 300 — это таймаут, но даже если бы мы это знали, то 300 чего? Миллисекунд? Секунд? Марсианских дней? И напротив, второй пример совершенно не требует объяснений.
Использование именованных аргументов — удобная возможность для языков, которые её поддерживают, но это не всегда возможно. Даже в Python, где
time.sleep
определяется с одним аргументом по имени secs
, мы не можем вызвать sleep(secs=300)
из-за особенностей реализации. В таком случае можно присвоить имя значению.Вместо этого:
time.sleep(300)
сделаем так:
sleep_seconds = 300
time.sleep(sleep_seconds)
Теперь в коде нет неоднозначностей, и он читаем даже без обращения к документации.
Вариант 2: использовать строгие типы
Вместо вставки единиц измерения в имя можно использовать более строгие типы, чем integer или float. Например, мы можем использовать тип duration.
Вместо этого:
def frobnicate(timeout: int) -> None:
...
frobnicate(300)
Сделаем вот так:
def frobnicate(timeout: timedelta) -> None:
...
timeout = timedelta(seconds=300)
frobnicate(timeout)
Чтобы иметь возможность интерпретировать единицу измерения заданного числа с плавающей запятой, необходимо как-то сообщить о ней. Если вам повезёт, то эта информация будет находиться в имени переменной или аргумента, но если не повезёт, то она будет указана лишь в документации, или не указана вовсе. Однако для значения
timedelta
нет неоднозначности интерпретаций, это часть типа. Кроме того, это устраняет неоднозначность из кода.Область применимости
Совет использовать строгие типы или вставлять единицы измерения в имена можно применять не только для переменных и аргументов функций, но и для API, имён метрик, форматов сериализации, файлов конфигураций, флагов командной строки и т. п. И хотя чаще всего единицы требуются для значений длительности, этот совет применим и к денежным величинам, длинам, размерам данных и т. п.
Например, возвращайте не такое:
{
"error_code": "E429",
"error_message": "Rate limit exceeded",
"retry_after": 100,
}
а такое:
{
"error_code": "E429",
"error_message": "Rate limit exceeded",
"retry_after_seconds": 100,
}
Не создавайте таких файлов конфигураций:
request_timeout = 10
лучше выберите один из этих вариантов:
request_timeout = 10s
request_timeout_seconds = 10
И не проектируйте бухгалтерское CLI-приложение таким образом:
show-transactions --minimum-amount 32
выберите один из этих вариантов:
show-transactions --minimum-amount-eur 32
show-transactions --minimum-amount "32 EUR"
Комментарии (107)
crea7or
31.05.2022 16:54+12надо же, плюсы не очень-то и отстают...
std::this_thread::wait_for(100ms);
artemisia_borealis
31.05.2022 23:15+2Хоть и не так давно завезли, но это очень полезно…
А (La)TeX так вообще лет сорок уже поддеживает\hspace{1.5cm} \addtolength{\hoffset}{-0.5mm} \addtolength{\textwidth}{1sp} \vspace{12pt} \rule{3mm}{.1pt}
DmitryMurinov
31.05.2022 17:20+12Телепортируемся в Java из редакции языка (и распространённых примеров) 1990-х в немного более свежую и пишем:
TimeUnit.MILLISECONDS.sleep(300); TimeUnit.MINUTES.sleep(5) и т.д..
WraithOW
31.05.2022 18:47+4Котлин и его экспериментальный time еще приятней
delay(300.milliseconds)mihmig
31.05.2022 22:04+2К чему эти ненужные скобки и точка? Они несут лишнюю когнитивную нагрузку.
Давайте придумаем новый язык, например Мотлин, в котором можно будет писать так:
delay 300 milliseconds
(Ведь код чаще читают, чем компилируют и пишут!)
И да, в новом языке "забудем" про статические методы, накостылив их через companion object!0xd34df00d
31.05.2022 22:13+7К чему эти ненужные скобки и точка?
Хаскелисты услышали вас. Там скобки в таких случаях не пишутся.
Статических методов там, к слову, тоже нет (за неимением объектов).
impwx
01.06.2022 12:35+7Как раз наоборот — до определенного предела знаки препинания позволяют быстрее понять, что именно за магия тут накручена. Например, если я вижу
delay(300.milliseconds)
, я сразу могу определить, что:- Вызывается метод
delay
с одним аргументом - Есть свойство
milliseconds
, скорее всего расширяемое, объявленное на типе целых чисел - Тип возвращаемого значения этого свойства совпадает с типом аргумента метода
delay
Если же я вижу
delay 300 milliseconds
, то можно интерпретировать эту конструкцию совершенно по-разному:- У метода
delay
два аргумента, первый — число, второй — некий алгебраический тип, перечисление единиц измерения времени, конструктором которого являетсяmilliseconds
- У метода
delay
один аргумент, аmilliseconds
— это постфиксный оператор (?), преобразующий число в специальный тип с диапазоном - У метода
delay
больше двух аргументов, и на этой строке мы только частично применили функцию, не вызвав ее — т.е. задержки не будет
Так что читаемость — это штука сугубо субъективная, и с количеством знаков препинания оно напрямую не коррелирует.
- Вызывается метод
leremin
31.05.2022 17:27+5В C# TimeSpan стараюсь использовать. Но вообще, по крайней мере в моей сфере, проблема глубже. Например, rotate(double angle) - градусы или радианы?
onyxmaster
31.05.2022 17:46+2Можно использовать доменные типы, у которых единица измерения указана в имени типа, например
rotate(Degrees angle)
. Вообще борьба с primitive obsession очень полезная штука (правда не во всех языках легко даётся).
Deosis
01.06.2022 07:10+2В f# пошли ещё дальше в ввели единицы измерения.
https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/units-of-measure
Таким образом можно сделать так:
[<Measure>] type rad let rotate (angle : double<rad>) = ...
И уже не получится случайно передать в функцию градусы.
DirectoriX
31.05.2022 18:07+9У использования специальных типов есть ещё одно преимущество перед абстрактными числами: если сложить 5 минут и 30 секунд — получится точно 5:30, а не 35… чего-то.
artemisia_borealis
31.05.2022 23:18-2Однако, появляются сложности с решением нефизических задач типа «найдите сторону квадрата у которого площадь равна периметру» и.т.п. :)
DirectoriX
31.05.2022 23:33+8Обычно спец. типы поддерживают преобразование обратно, например время в Rust
Duration::from_millis(123).as_secs_f32(); //обычное число
Ну а если вдруг типы настолько умные, что длина * длина = площадь (и нет преобразования обратно в безразмерное число), то вы всегда можете периметр домножить на 1 единицу длины и получить то же числовое значение, но уже в виде площади.
SergeiMinaev
31.05.2022 18:24+10sleep_seconds = 300
time.sleep(sleep_seconds)
Зачем писать два раза sleep? Вполне хватит
sleep(secs)
. Излишняя подробность тоже утомляет.Даже в Python, где
time.sleep
определяется с одним аргументом по имениsecs
, мы не можем вызватьsleep(secs=300)
из-за особенностей реализации.Можем:
sleep(secs:=300)
.
SadOcean
31.05.2022 18:50+13О да, вспоминаю прекрасную функцию в нашем проекте, которая имела 2 оверлоада
- int в мс
- float в сек
Timer.Show(1, () => {});
и
Timer.Show(1f, () => {});
Различались по работе всего в 1000 раз.fougasse
31.05.2022 20:14-4Как это прошло код-ревью?
slonopotamus
31.05.2022 20:31-1А даже если как-то и прошло, почему не было выправлено тут же как только проблема была обнаружена?
SadOcean
01.06.2022 11:20+2Легко, если изменения сделаны в разное время.
По отдельности каждая функция выглядит достаточно разумно, диф при изучении не вызывает подозрений.
PowerMetall
01.06.2022 15:48Timer.Show(1, () => {});
и
Timer.Show(1f, () => {});Особенно весело наверное выглядело бы в коде, например
var timeToShow = 100; // да да, название переменной не соответствует теме статьи )) ... // куча // некого // кода ... Timer.Show(t, () => {});
tormozedison
31.05.2022 18:58-1С одной стороны — «идеи витают в воздухе», с другой — «ну почему я сам до этого не додумался».
agalakhov
31.05.2022 19:10+3Используйте типы.
sleep(Seconds(10));
sleep(10.ms()); // в Rust можно такincludedlibrary
31.05.2022 19:17+5Намного лучше префиксов, как по мне. Нельзя просто так взять и int засунуть, нужно сначала его в Seconds преобразовать. Сразу понятно становится, с какой единецей измерения ты работаешь
igrishaev
31.05.2022 20:28А как потом воевать с классами `com.google.Seconds`, `com.apple.util.Seconds`, `com.netflix.time.Seconds`, `org.someshit.Seconds`?
cepera_ang
31.05.2022 21:52-1Трейты. Если секунды ведут себя как секунды, то и пофиг из какого оргсамшита они приехали.
0xd34df00d
31.05.2022 21:56Что значит «ведут себя как секунды»?
cepera_ang
31.05.2022 22:05+3Передаются в функции ждущие секунды, конвертируются в минуты, тикают :)
0xd34df00d
31.05.2022 22:14+1То есть, у вас уже в самом языке есть отдельный тип под секунды? Не, ну это читерство. Зачем тогда гугловские и эппловские секунды?
Cerberuser
01.06.2022 06:27Не тип, а 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 (но опять-таки, это именно "стандарт де-факто", а не "один из многих").
0xd34df00d
01.06.2022 08:00+1Я бы посмотрел, как бы это всё выглядело без втягивания какого-то типа для времени в стандартную библиотеку и без передачи времён как сырых секунд/наносекунд. Потому что эти типы для времени вылезают здесь, например:
вероятнее, правда, это всё-таки будет тайпкласс Duration, без явной привязки к конкретной единице измерения, но с возможностью конвертироваться в них
Cerberuser
01.06.2022 08:06Я имею в виду, что функция, принимающая impl Duration, сама может дёрнуть на нём seconds(), milliseconds() или что там ей нужно, чтобы получить сырое число для передачи в низкоуровневый метод (системный вызов или FFI). Либо продолжит работать с impl Duration как с чёрным ящиком, который можно прибавлять к impl Instant для получения другого impl Instant и сравнения, например, с Instant::now.
0xd34df00d
01.06.2022 08:28+1Я имею в виду, что функция, принимающая impl Duration, сама может дёрнуть на нём seconds(), milliseconds() или что там ей нужно, чтобы получить сырое число для передачи в низкоуровневый метод (системный вызов или FFI).
Да, это я понял, но это ведь как раз подмножество типов, связанных со временем, но в ядре языка.
Либо продолжит работать с impl Duration как с чёрным ящиком, который можно прибавлять к impl Instant для получения другого impl Instant и сравнения, например, с Instant::now.
Спать рано или поздно придётся в конкретных единицах времени, увы.
Cerberuser
01.06.2022 08:43это ведь как раз подмножество типов, связанных со временем, но в ядре языка.
Не совсем понимаю, что за "подмножество". Есть один тайпкласс, который предоставляет несколько возможностей получить условный Integer, в зависимости от того, что нужно нижележащему слою. О каких типах тут речь?
Спать рано или поздно придётся в конкретных единицах времени, увы.
А как, по Вашему опыту, может быть реализовано это "спать"? Я вижу два варианта: либо упомянутый syscall/FFI (которым, с точки зрения типов, нужен именно "условный Integer"), либо "периодически просыпаться, проверять, не пора ли продолжать, если не пора - спать дальше", который прекрасно реализуется по принципу "запомнили impl Instant в начале сна и impl Duration на его продолжительность, при пробуждении проверяем, что start + duration < now".
0xd34df00d
01.06.2022 08:52О каких типах тут речь?
В данном случае — о самом этот тайпклассе.
А как, по Вашему опыту, может быть реализовано это "спать"? Я вижу два варианта [...]
Я говорил о первом, да. Второй может сработать, но есть проблемы с гранулярностью против потребления ресурсов.
Cerberuser
01.06.2022 09:01Ну, изначально-то разговор был о том, как в Rust справляются с ситуациями, когда есть пачка разных реализаций одного и того же функционала (т.е. когда уже есть - ну, или предвидится - наличие этого самого "подмножества типов"). Ответ именно в этом - стандартный (де-юре или де-факто) тайпкласс, который реализуют все, кто хочет вписаться в экосистему.
SadOcean
01.06.2022 12:50Значит "реализуют интерфейс, характерный для секунд"
Трейты можно реализовывать вне объявления типа, поэтому можно дописывать типы под нужды библиотек.
Соответственно через трейты можно теоретически объеденить несколько библиотек, считающих время, просто при вызовах типы будут прозрачно конвертироваться или реализовывать нужные интерфейсы поверх.
igrishaev
01.06.2022 09:44+1Отлично. А чей трейт использовать -- com.google.TraitSeconds, com.apple.TraitSeconds, org.someshit.TraitSeconds?
Cerberuser
01.06.2022 10:50-1rs.time.TraitSeconds, который будут реализовывать все, кто хочет, чтобы их использовали не только в рамках их собственной узкой ниши - и google, и apple, и someshit.
igrishaev
01.06.2022 10:53+2Значит, для начала надо затащить rs.time.TraitSeconds в стандартную библиотеку. А потом еще градусы цельсия, килограммы, радианы, ньютоны, амперы, года, световые года и прочее. Кто этим будет заниматься?
Cerberuser
01.06.2022 10:55Ну, вышеупомянутый serde же кто-то сделал. И ему совсем не обязательно становиться стандартом де-юре.
AnthonyMikh
01.06.2022 20:09А зачем это в стандартную библиотеку затаскивать? Для того, чтобы интерфейс из библиотеки стал общеупотребимым, не обязательно, чтобы библиотека была стандартной библиотекой языка.
Savevur
31.05.2022 19:13-1это таймаут, но даже если бы мы это знали, то 300 чего? Миллисекунд? Секунд? Марсианских дней?
Если ничего не указано, то значит соответствует международной системе единиц этой планеты. Зачем лишнее писать, для марсиан? Не, ну если раскроют их число, и их окажется больше чем всех остальных, тогда да.
SergeiMinaev
31.05.2022 19:58+1Если ничего не указано, то значит
Не всегда. Timestamp в JS - миллисекунды, в Python - секунды.
Savevur
31.05.2022 20:36Так тема то о другом - о том, чтобы мы код друг друга поняли. А что там в JS, Python внутри - это мы с вами по определению должны знать без доп обёрток.
Немного непонятно я написал. Ок. Если я получил от вас код, и у вашей переменной, которой вы сами придумали имя, нет постфикса единицы измерения, то я предполагаю, что в переменной вы храните величину в единицах СИ.
fougasse
31.05.2022 20:18+3А потом вы оказываетесь в США с футами, милями, и унциями. И ваш условный
double getEnergyContent(double weight);
летит в тар-тарары
Savevur
31.05.2022 20:31Это не мой, а ваш условный. Если практики ни в чем нет, в том числе в интерфейсах международных программ, то можно улететь и от более простых вещей. Всё внутри движка делать в единицах СИ, и ни к чему там префиксы-постфиксы.
falconandy
31.05.2022 19:20+3В Go используется тип
time.Duration
:time.Sleep(time.Second * 30) time.Sleep(time.Millisecond * 300) time.Sleep(300) // nanoseconds
Savevur
31.05.2022 19:34+1В MathCAD красота.
konst90
31.05.2022 20:44+1Да, это великая штука. Переваривает все возможные размерности, что дико упрощает работу, особенно со всякими справочниками. Тут миллиметры, там метры, здесь мегапаскали, тут килограммы на квадратный метр, а вот у этого американского материала фунты на квадратный дюйм - отлично, не забудь это указать, и получишь нужный результат без дополнительных усилий по конвертации. Заодно и дополнительный контроль, потому что в ответе число тоже будет с размерностью, и если она не та - это сразу видно.
slonopotamus
01.06.2022 17:08+2time.Duration
- это недоразумение, а не типизация.Вот это вот компилируется:
time.Sleep(time.Second * time.Second)
falconandy
01.06.2022 17:29Внутри
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)
slonopotamus
01.06.2022 19:15+2Внутри
Duration
это всего лишьint64
То что есть zero-cost абстракции - это совершенно нормально. Ненормально что эта конкретная абстракция дырява чуть менее чем полностью.
Вот так кстати тоже можно:
time.Sleep(time.Second + 42)
Складываем яблоки с апельсинами, а чо бы и нет.
falconandy
01.06.2022 20:23-1В данном случае 42 — это не апельсины, а 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)
Cerberuser
02.06.2022 06:51В данном случае 42 — это не апельсины, а 42 наносекунды (те же яблоки).
Ну то есть таки яблоки с апельсинами (секунды с наносекундами). Да, и то и другое -
фруктыединицы времени, но толку, если оно читается не так, как работает?falconandy
02.06.2022 08:25Нет, не яблоки с апельсинами, а только яблоки — «время». Не уверен, как оно для вас читается (секунда плюс 42 секунды?), но в данном конректном кейсе выглядит опечаткой как минимум — возможно вместо
*
ошибочно написан+
.
Аналогичный тип для «массы» выглядел бы примерно так:type Mass int64 const ( Milligram Mass = 1 Gram = 1000 * Milligram Kilogram = 1000 * Gram ) var mass = Gram + 42
Cerberuser
02.06.2022 08:32time.Second + 42 читается как "секунда + 42 секунды", если не знать, что time.Second - это "миллиард наносекунд", а не "секунда". Точно так же и Gram + 42 читается на первый взгляд не как "1000 миллиграмм + 42 миллиграмма", а как "грамм + 42 грамма".
AnthonyMikh
01.06.2022 20:10+1Но
time.Sleep
всё ещё можно скормить голое число. Выглядит не очень надёжно.falconandy
01.06.2022 20:24Написал выше про возможную доработку линтера
durationcheck
gecube
02.06.2022 00:14там вообще чисел не должно быть... Я почему-то понимаю, как это написать на С++ на шаблонах... Но вот как это сделать на golang'е или пыхыпы, простите, не представляю...
konst90
31.05.2022 20:39С инженерной колокольни я сказал бы - знай язык, на котором работаешь. Например, если я читаю данные с чертежа и вношу их в скрипт на языке прочностного пакета ANSYS - то я знаю, что на чертеже размеры будут в миллиметрах, а пакет прочитает переменную из скрипта как метры. И соответственно не забуду поделить число на тысячу - либо в уме, либо добавив в скрипт эту операцию.
Почему у программистов это не так? Языков много, и для всех помнить не получается? Или есть другие причины?
F0iL
31.05.2022 20:49+6"Знай язык" - это про то, что входит в его стандартную библиотеку (и как вы метко заметили, стандартные библиотеки в разных языках могут быть очень разными). А код, который принимает в качестве аргументов не абстрактные числа, а те которые именно что-то значат (секунды, дни, пиксели, байты, градусы, и т.д.) может быть и не из стандартной библиотеки, а из самого проекта (а в каждом проекте используются те единицы, что удобнее конкретно в нём).
Savevur
31.05.2022 21:03+5Почему у программистов это не так?
Потому что нет стандартов. Статья начинается со слов "Есть одна ловушка читаемости кода". (Проблема читаемости кода, передаваемого другим людям.) Пакет ANSYS и языки программирования - частные случаи проблемы понимания передаваемых знаний и инструментов (код, чертеж) одного человека другим человеком.
На вашем примере. Вы получили на бумаге с курьером чертеж, на котором не указан масштаб. И вы не знаете на тысячу, сотню делить или вообще не делить. Но это нонсенс. Масштаб обязан быть проставлен по стандарту планеты. А вот у программиста в основном нет требований, если нет каких-то корпоративных стандартов. Т.е. если даже стандарт и есть, то он одной фирмы, а у другой - другой. Мирового нет.
mjr27
31.05.2022 21:56+1Thread.sleep(3000); //Ms
не благодарите
cepera_ang
31.05.2022 22:06+4> // Ms
Microsoft? Michael Schumacher? Megaseconds?mayorovp
01.06.2022 22:20+4Microsoft и Michael Schumacher тут по контексту не подходят. Разумеется, этот комментарий может означать только "мегасекунды".
0xd34df00d
31.05.2022 22:28+6Комментарий не проверяется компилятором, комментарий может устареть, комментарий не переведёт за вас в другие юниты.
mjr27
01.06.2022 07:09+1Типа предложенное автором sleep_seconds = 3000 проверяется компилятором и.не может устареть, ага.
Конечно, если есть возможность, обычно стоит передавать TimeSpan/timedelta/duration. Если нет, этот способ ничем не хуже любого другого.
leremin
31.05.2022 22:17Кстати, вспомнилось OpenXml. Там же дичь с единицами измерений шрифтов, полей, отступов. Где что сейчас не вспомню, но там и миллиметры, и дюймы, и 1/1440 дюйма, и проценты, и ещё невесть что.
Hlad
01.06.2022 08:22Эмм, а обычные комментарии — это уже не модно?
Вообще, за концепцию «хороший код должен читаться без комментариев, поэтому комментарии не нужны» надо бить. Оно, конечно, на каком-то самом низком уровне намного проще и приятнее разбираться в коде типаfor NumberOfRecords = 0 to MaxRecordNumber...
Немного проще, чем вfor i=0 to a
Но развёрнутый комментарий позволяет написать не только «что мы делаем», но и «зачем мы это делаем».dopusteam
01.06.2022 10:13На самом деле, если дописать тело цикла, то будет намного понятен профит, когда будет обращение не к i, а к numberOfRecords
Vindicar
01.06.2022 11:01Мне доводилось писать физические расчёты на Питоне, используя pint для контроля размерности. Проблемы возникали в нескольких местах:
загрузка/сохранение конфигураций. Пришлось городить костыль для хранения единиц измерения рядом со значением.
Любой алгоритм, требущий сторонней библиотеки, приводит к необходимости сначала оторвать единицы измерения от исходных данных, а потом вручную добавить нужные единицы к результатам.
То и дело требуется явно проверить размерность, или явно привести к заданной размерности. Например, pint любит разбирать ньютоны (Н) на составные части (кг*м / с^2), что неудобно при выводе.
Пока нет поддержки типизации, т.е. нельзя сделать type hint с указанием размерности.
Но в целом не скажу, что pint не стоил затраченных усилий.
Panda_sama
01.06.2022 11:34+2Глядя на запись "ждать 300 чего-то" - пусть даже там как-то будет указано "300 секунд" или "300 минут", у меня прежде всего возникнет вопрос - а почему именно 300?
Мне кажется, в подобной ситуации напрашивается введение константы (в название которой можно указать sec300 или ms300) и использование в sleep(ms300) именно её
baldr
03.06.2022 13:13+1Отличное и правильное предложение.
Однако не спасет от:
sleep(something.get_duration())
Color
01.06.2022 14:27-1Это все последствия слабой системы типов.
Пример, как это сделано правильно: в Golang есть тип `time.Duration`, в который можно передать данные в любом виде - миллисекунды, минуты, часы или что хотите. В итоге вопросов вида "что лежит в переменной" не возникает - там лежит длительность с абсолютной размерностью, которую при необходимости (в случае вывода в консоль, например) можно преобразовать к любой нужной величине - секундам, минутам и т.п.
questor
02.06.2022 22:32Поэтому у нас на кодревью принято заворачивать подобные конструкции и использовать спаны (C#):
await Task.Delay(TimeSpan.FromSeconds(3));
Keeper1
Всё уже придумано до нас: https://ru.wikipedia.org/wiki/Венгерская_нотация
fougasse
И как она поможет? В контексте единиц времени.
m — это минута или месяц? Зачем запоминать совсем неочевидные сокращения? Экономить место, чернила в принтере листингов? Сколько их будет, сокращений?
А если не только время, но и длину? А вес? А денежные операции?
Keeper1
Никто не мешает придумать свои префиксы и их невозбранно использовать. Например, min, sec и так далее. Аналогично для длины, веса и прочего.
gecube
эти префиксы не проверяются на уровне типов. В результате они не спасают ни от опечаток, ни от недопонимания, ни от ошибок в документации
Keeper1
В исходной статье про типы - ни слова. Только про правила именования. Что касается системы типов и их проверки - это уже совершенно другая, отдельная проблема иного класса сложности.
P.S. Господи, ещё один адепт секты всеобщей типизации. Откуда вы только лезете в таких количествах?
0xd34df00d
Из основ теоретической информатики и матлогикиА что вам не нравится в типах?Keeper1
О типах как абстракции ничего плохого сказать не могу. (Возможно, есть смысл говорить о реализации систем типов в конкретных языках программирования, вот там можно будет найти много неудачного.)
Мне не нравятся фанатики. «Только ситхи всё возводят в абсолют».
0xd34df00d
Большинство из реализаций не основывалось на достаточно формальных принципах, увы.
Не, конечно, для одноразовых скриптов без особой логики на 20 строк хардкорная типизация действительно не нужна.
dom1n1k
Тоже сразу вспомнил ВН, а точнее мегастатью с критикой ВН, которую я читал много-много лет назад. Там автор в красках (хотя и не особо убедительно, на мой вкус) описывал, почему ВН это грязный и ненужный костыль - и в том числе на примере единиц измерения.
Keeper1
Вот именно, всё это уже давно известно, обсуждено и обсосано. А описанное в статье -- это
венгерская нотация курильщикаочередной велосипед.fougasse
Вы пытаетесь решить самую большую и сложную проблему Computer Science — именование переменных. ВН не особо помогла за десятки лет, а, скорее, налборот, увела часть индустрии не туда.
Keeper1
Я даже не пытаюсь.
gro
Другой автор в красках описывал, как ВН может быть полезна, если её правильно готовить: https://www.joelonsoftware.com/2005/05/11/making-wrong-code-look-wrong/
Но не понимаю, как это можно применить к единицам измерения.
ivegner
Речь о том, что число само по себе может быть интерпретировано как угодно и в этом его слабость. В предметной области скорее всего каждому числу соответствует то, что измеряют, и единица, в которой измеряют. В этом смысле даже "позиция элемента в списке" — это единица измерения. Как и секунда, как и валюта, как и пиксель, и автоинкрементный айдишник в базе. Будет нотация венгерской или какой — второстепенно. Главная мысль — выражайте явнее, какое число что измеряет и в какой единице. Просто путаница с секундами и миллисекундами это самый классический пример, в приведённой Вами статье есть ещё немало хороших примеров. Или вот прямо сегодня у меня коллега смешал в одну кучу переменные, которые измеряют сумму денег в единицах валют (грубо говоря, в евро) и в сотых долях этих валют (в евроцентах). В результате все шансы попасть на откат неправильно посчитанных операций. А можно было и не попадать, если в имени каждой переменной явно указать, это единицы или сотые доли. Вот и вся сказка.
AnthonyMikh
Пример не убедителен, при помощи типов это решается в разы надёжнее.
gecube
полностью поддерживаю
@gro там хороший пример, но надо понимать, что венгерская нотация в духе "Systems Hungarian" - это бред. Семантическая (которая "Apps Hungarian") - ее вообще язык венгерской нотацией не поворачивается назвать... А вполне себе просто нормальный способ именования переменных, но все равно он не поможет в случае, когда у Вас есть контейнер для времени, и куча возможностей засунуть туда разные единицы измерения. Примерно как в случае@ivegner Чем мне поможет наличие разных названий у переменных, если компилятор мне по рукам не даст при их приведении друг к другу? К сожалению, это может сделать только система типов.
@Keeper1 а почему префиксы должны быть одни, а не другие? Или скажем, у Вас будет префикс mGeorg и mKeeper - и как определить - какой из них для месяца (month), а какой для минуты? Или почему не делать это на другом языке, скажем, венгерском - perc для минуты? Или еще как-то? Я понимаю в системе типов, там хотя бы жестко ограничиваешь возможности перехода туда-сюда...
Keeper1
Цитирую себя же:
Аналогично для венгерского языка. Главное, чтобы 1С не получился.
gecube
не нужно. Почему - я объяснил. Лучше вообще типизированный объект Time(minutes=60, seconds=10)....