Pull to refresh

Comments 114

И как она поможет? В контексте единиц времени.

m — это минута или месяц? Зачем запоминать совсем неочевидные сокращения? Экономить место, чернила в принтере листингов? Сколько их будет, сокращений?

А если не только время, но и длину? А вес? А денежные операции?

Никто не мешает придумать свои префиксы и их невозбранно использовать. Например, min, sec и так далее. Аналогично для длины, веса и прочего.

эти префиксы не проверяются на уровне типов. В результате они не спасают ни от опечаток, ни от недопонимания, ни от ошибок в документации

В исходной статье про типы - ни слова. Только про правила именования. Что касается системы типов и их проверки - это уже совершенно другая, отдельная проблема иного класса сложности.

P.S. Господи, ещё один адепт секты всеобщей типизации. Откуда вы только лезете в таких количествах?

UFO just landed and posted this here

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

Мне не нравятся фанатики. «Только ситхи всё возводят в абсолют».

UFO just landed and posted this here

Тоже сразу вспомнил ВН, а точнее мегастатью с критикой ВН, которую я читал много-много лет назад. Там автор в красках (хотя и не особо убедительно, на мой вкус) описывал, почему ВН это грязный и ненужный костыль - и в том числе на примере единиц измерения.

Вот именно, всё это уже давно известно, обсуждено и обсосано. А описанное в статье -- это венгерская нотация курильщика очередной велосипед.

Вы пытаетесь решить самую большую и сложную проблему Computer Science — именование переменных. ВН не особо помогла за десятки лет, а, скорее, налборот, увела часть индустрии не туда.

Речь о том, что число само по себе может быть интерпретировано как угодно и в этом его слабость. В предметной области скорее всего каждому числу соответствует то, что измеряют, и единица, в которой измеряют. В этом смысле даже "позиция элемента в списке" — это единица измерения. Как и секунда, как и валюта, как и пиксель, и автоинкрементный айдишник в базе. Будет нотация венгерской или какой — второстепенно. Главная мысль — выражайте явнее, какое число что измеряет и в какой единице. Просто путаница с секундами и миллисекундами это самый классический пример, в приведённой Вами статье есть ещё немало хороших примеров. Или вот прямо сегодня у меня коллега смешал в одну кучу переменные, которые измеряют сумму денег в единицах валют (грубо говоря, в евро) и в сотых долях этих валют (в евроцентах). В результате все шансы попасть на откат неправильно посчитанных операций. А можно было и не попадать, если в имени каждой переменной явно указать, это единицы или сотые доли. Вот и вся сказка.

Другой автор в красках описывал, как ВН может быть полезна, если её правильно готовить:

Пример не убедителен, при помощи типов это решается в разы надёжнее.

полностью поддерживаю

@gro там хороший пример, но надо понимать, что венгерская нотация в духе "Systems Hungarian" - это бред. Семантическая (которая "Apps Hungarian") - ее вообще язык венгерской нотацией не поворачивается назвать... А вполне себе просто нормальный способ именования переменных, но все равно он не поможет в случае, когда у Вас есть контейнер для времени, и куча возможностей засунуть туда разные единицы измерения. Примерно как в случае@ivegner Чем мне поможет наличие разных названий у переменных, если компилятор мне по рукам не даст при их приведении друг к другу? К сожалению, это может сделать только система типов.

@Keeper1 а почему префиксы должны быть одни, а не другие? Или скажем, у Вас будет префикс mGeorg и mKeeper - и как определить - какой из них для месяца (month), а какой для минуты? Или почему не делать это на другом языке, скажем, венгерском - perc для минуты? Или еще как-то? Я понимаю в системе типов, там хотя бы жестко ограничиваешь возможности перехода туда-сюда...

Цитирую себя же:

Никто не мешает придумать свои префиксы и их невозбранно использовать. Например, min, sec и так далее. Аналогично для длины, веса и прочего.

Аналогично для венгерского языка. Главное, чтобы 1С не получился.

не нужно. Почему - я объяснил. Лучше вообще типизированный объект Time(minutes=60, seconds=10)....

надо же, плюсы не очень-то и отстают...

std::this_thread::wait_for(100ms);

Хоть и не так давно завезли, но это очень полезно…

А (La)TeX так вообще лет сорок уже поддеживает

\hspace{1.5cm}
\addtolength{\hoffset}{-0.5mm}
\addtolength{\textwidth}{1sp}
\vspace{12pt}
\rule{3mm}{.1pt}
UFO just landed and posted this here
Котлин и его экспериментальный time еще приятней
delay(300.milliseconds)

К чему эти ненужные скобки и точка? Они несут лишнюю когнитивную нагрузку.
Давайте придумаем новый язык, например Мотлин, в котором можно будет писать так:
delay 300 milliseconds
(Ведь код чаще читают, чем компилируют и пишут!)
И да, в новом языке "забудем" про статические методы, накостылив их через companion object!

UFO just landed and posted this here

Как раз наоборот — до определенного предела знаки препинания позволяют быстрее понять, что именно за магия тут накручена. Например, если я вижу delay(300.milliseconds), я сразу могу определить, что:


  1. Вызывается метод delay с одним аргументом
  2. Есть свойство milliseconds, скорее всего расширяемое, объявленное на типе целых чисел
  3. Тип возвращаемого значения этого свойства совпадает с типом аргумента метода delay

Если же я вижу delay 300 milliseconds, то можно интерпретировать эту конструкцию совершенно по-разному:


  1. У метода delay два аргумента, первый — число, второй — некий алгебраический тип, перечисление единиц измерения времени, конструктором которого является milliseconds
  2. У метода delay один аргумент, а milliseconds — это постфиксный оператор (?), преобразующий число в специальный тип с диапазоном
  3. У метода delay больше двух аргументов, и на этой строке мы только частично применили функцию, не вызвав ее — т.е. задержки не будет

Так что читаемость — это штука сугубо субъективная, и с количеством знаков препинания оно напрямую не коррелирует.

infix уже есть в котлине

В C# TimeSpan стараюсь использовать. Но вообще, по крайней мере в моей сфере, проблема глубже. Например, rotate(double angle) - градусы или радианы?

Можно использовать доменные типы, у которых единица измерения указана в имени типа, например rotate(Degrees angle). Вообще борьба с primitive obsession очень полезная штука (правда не во всех языках легко даётся).

Если были бы радианы, было бы подписано radians
Но в целом актуально.

Если были бы градусы - было бы подписано "degrees". Радианы - единица измерения углов по умолчанию во многих "небытовых" контекстах.

вот так методом исключения пришли к выводу, что там грады

В f# пошли ещё дальше в ввели единицы измерения.

https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/units-of-measure

Таким образом можно сделать так:

[<Measure>] type rad
let rotate (angle : double<rad>) = ...

И уже не получится случайно передать в функцию градусы.

У использования специальных типов есть ещё одно преимущество перед абстрактными числами: если сложить 5 минут и 30 секунд — получится точно 5:30, а не 35… чего-то.
Однако, появляются сложности с решением нефизических задач типа «найдите сторону квадрата у которого площадь равна периметру» и.т.п. :)
Обычно спец. типы поддерживают преобразование обратно, например время в Rust
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).

Так-то можно и sleep(kilogram:=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`?

UFO just landed and posted this here
UFO just landed and posted this here
UFO just landed and posted this here
UFO just landed and posted this here

Не тип, а 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 (но опять-таки, это именно "стандарт де-факто", а не "один из многих").

UFO just landed and posted this here

Я имею в виду, что функция, принимающая impl Duration, сама может дёрнуть на нём seconds(), milliseconds() или что там ей нужно, чтобы получить сырое число для передачи в низкоуровневый метод (системный вызов или FFI). Либо продолжит работать с impl Duration как с чёрным ящиком, который можно прибавлять к impl Instant для получения другого impl Instant и сравнения, например, с Instant::now.

UFO just landed and posted this here

это ведь как раз подмножество типов, связанных со временем, но в ядре языка.

Не совсем понимаю, что за "подмножество". Есть один тайпкласс, который предоставляет несколько возможностей получить условный Integer, в зависимости от того, что нужно нижележащему слою. О каких типах тут речь?

Спать рано или поздно придётся в конкретных единицах времени, увы.

А как, по Вашему опыту, может быть реализовано это "спать"? Я вижу два варианта: либо упомянутый syscall/FFI (которым, с точки зрения типов, нужен именно "условный Integer"), либо "периодически просыпаться, проверять, не пора ли продолжать, если не пора - спать дальше", который прекрасно реализуется по принципу "запомнили impl Instant в начале сна и impl Duration на его продолжительность, при пробуждении проверяем, что start + duration < now".

UFO just landed and posted this here

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

Значит "реализуют интерфейс, характерный для секунд"
Трейты можно реализовывать вне объявления типа, поэтому можно дописывать типы под нужды библиотек.
Соответственно через трейты можно теоретически объеденить несколько библиотек, считающих время, просто при вызовах типы будут прозрачно конвертироваться или реализовывать нужные интерфейсы поверх.

Отлично. А чей трейт использовать -- com.google.TraitSeconds, com.apple.TraitSeconds, org.someshit.TraitSeconds?

rs.time.TraitSeconds, который будут реализовывать все, кто хочет, чтобы их использовали не только в рамках их собственной узкой ниши - и google, и apple, и someshit.

Значит, для начала надо затащить rs.time.TraitSeconds в стандартную библиотеку. А потом еще градусы цельсия, килограммы, радианы, ньютоны, амперы, года, световые года и прочее. Кто этим будет заниматься?

Ну, вышеупомянутый serde же кто-то сделал. И ему совсем не обязательно становиться стандартом де-юре.

А зачем это в стандартную библиотеку затаскивать? Для того, чтобы интерфейс из библиотеки стал общеупотребимым, не обязательно, чтобы библиотека была стандартной библиотекой языка.

UFO just landed and posted this here

Если ничего не указано, то значит

Не всегда. Timestamp в JS - миллисекунды, в Python - секунды.

UFO just landed and posted this here

А потом вы оказываетесь в США с футами, милями, и унциями. И ваш условный

double getEnergyContent(double weight);

летит в тар-тарары

UFO just landed and posted this here

А если код написан на Земле, но выполняется на Луне, то что?

В Go используется тип time.Duration:
time.Sleep(time.Second * 30)
time.Sleep(time.Millisecond * 300)

time.Sleep(300) // nanoseconds
UFO just landed and posted this here

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

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)

Складываем яблоки с апельсинами, а чо бы и нет.

В данном случае 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)

В данном случае 42 — это не апельсины, а 42 наносекунды (те же яблоки).

Ну то есть таки яблоки с апельсинами (секунды с наносекундами). Да, и то и другое - фрукты единицы времени, но толку, если оно читается не так, как работает?

Нет, не яблоки с апельсинами, а только яблоки — «время». Не уверен, как оно для вас читается (секунда плюс 42 секунды?), но в данном конректном кейсе выглядит опечаткой как минимум — возможно вместо * ошибочно написан +.

Аналогичный тип для «массы» выглядел бы примерно так:
type Mass int64

const (
	Milligram Mass = 1
	Gram           = 1000 * Milligram
	Kilogram       = 1000 * Gram
)

var mass = Gram + 42

time.Second + 42 читается как "секунда + 42 секунды", если не знать, что time.Second - это "миллиард наносекунд", а не "секунда". Точно так же и Gram + 42 читается на первый взгляд не как "1000 миллиграмм + 42 миллиграмма", а как "грамм + 42 грамма".

Но time.Sleep всё ещё можно скормить голое число. Выглядит не очень надёжно.

Написал выше про возможную доработку линтера durationcheck

там вообще чисел не должно быть... Я почему-то понимаю, как это написать на С++ на шаблонах... Но вот как это сделать на golang'е или пыхыпы, простите, не представляю...

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

Почему у программистов это не так? Языков много, и для всех помнить не получается? Или есть другие причины?

UFO just landed and posted this here
UFO just landed and posted this here
UFO just landed and posted this here

Микаэль Шумахер, да

Microsoft и Michael Schumacher тут по контексту не подходят. Разумеется, этот комментарий может означать только "мегасекунды".

UFO just landed and posted this here

Типа предложенное автором sleep_seconds = 3000 проверяется компилятором и.не может устареть, ага.

Конечно, если есть возможность, обычно стоит передавать TimeSpan/timedelta/duration. Если нет, этот способ ничем не хуже любого другого.

Кстати, вспомнилось OpenXml. Там же дичь с единицами измерений шрифтов, полей, отступов. Где что сейчас не вспомню, но там и миллиметры, и дюймы, и 1/1440 дюйма, и проценты, и ещё невесть что.

еще можно так

let sleepSeconds = sleep

sleepSeconds(5)

UFO just landed and posted this here
Эмм, а обычные комментарии — это уже не модно?
Вообще, за концепцию «хороший код должен читаться без комментариев, поэтому комментарии не нужны» надо бить. Оно, конечно, на каком-то самом низком уровне намного проще и приятнее разбираться в коде типа
for NumberOfRecords = 0 to MaxRecordNumber...
Немного проще, чем в
for i=0 to a
Но развёрнутый комментарий позволяет написать не только «что мы делаем», но и «зачем мы это делаем».

На самом деле, если дописать тело цикла, то будет намного понятен профит, когда будет обращение не к i, а к numberOfRecords

Мне доводилось писать физические расчёты на Питоне, используя pint для контроля размерности. Проблемы возникали в нескольких местах:

  • загрузка/сохранение конфигураций. Пришлось городить костыль для хранения единиц измерения рядом со значением.

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

  • То и дело требуется явно проверить размерность, или явно привести к заданной размерности. Например, pint любит разбирать ньютоны (Н) на составные части (кг*м / с^2), что неудобно при выводе.

  • Пока нет поддержки типизации, т.е. нельзя сделать type hint с указанием размерности.

Но в целом не скажу, что pint не стоил затраченных усилий.

Глядя на запись "ждать 300 чего-то" - пусть даже там как-то будет указано "300 секунд" или "300 минут", у меня прежде всего возникнет вопрос - а почему именно 300?

Мне кажется, в подобной ситуации напрашивается введение константы (в название которой можно указать sec300 или ms300) и использование в sleep(ms300) именно её

Отличное и правильное предложение.

Однако не спасет от: sleep(something.get_duration())

Это все последствия слабой системы типов.

Пример, как это сделано правильно: в Golang есть тип `time.Duration`, в который можно передать данные в любом виде - миллисекунды, минуты, часы или что хотите. В итоге вопросов вида "что лежит в переменной" не возникает - там лежит длительность с абсолютной размерностью, которую при необходимости (в случае вывода в консоль, например) можно преобразовать к любой нужной величине - секундам, минутам и т.п.

UFO just landed and posted this here
UFO just landed and posted this here

Поэтому у нас на кодревью принято заворачивать подобные конструкции и использовать спаны (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)

Но увы.

Sign up to leave a comment.