...после ленивой годичной практики языка, я все же был вынужден заключить, что в разрезе embedded программирования (где можно и нужно активно использовать программирование времени компиляции) Rust не только не делает качественно скачка вперед, но и даже в чем-то уступает C++ в 2024 году.
А вот про это можно поподробнее? В чём уступает? Мне на ум только меньший охват целевых платформ приходит.
А я вот прошёл Doom Eternal первым и потом прошёл Doom 2016 через силу. Потому что в плане геймплея Doom 2016 уступает почти по всем аспектам, сюжет более невнятный, а одинаковая оранжевая гамма всех уровней надоедает
Нет, зачем нужны аргументы со значениями по умолчанию - понятно, это много где есть. Зачем докидывать undefined, если аргументов недостаточно - неясно. Недостаточное количество аргументов при вызове - это ошибка на вызывающей стороне, а поведение JavaScript способствует тому, чтобы эти ошибки обнаруживались позже, чем надо.
Далеко не всегда для корректного кода на JS тип выводится автоматом.
Если вы пишите на Javascript код, для которого специально построенный для вывода типов инструмент не может вывести типы, то с чего вы считаете, что эти типы будут ясны человеку, который этот код считает?
Я, конечно, потратил некоторое время, чтобы придумать синтетический пример с большим количеством проверок на пограничные кейсы. Не сколько предендующий на грамотность, сколько на показательность в контексте данной статьи.
Более приближенный к реальности пример был бы убедительнее.
Давайте посмотрим на типичную функцию с некоторым количеством проверок на пограничные состояния:
Давайте, и я вам покажу, что дело не в отсутствии ранних возвратов, а в кривой архитектуре. А чтобы разговор был более конкретным - напишу минимальный дополнительный код, чтобы он компилировался:
Для начала, зачем вообще принимать Spell по указателю? Указатель может быть null, и это то, что нам никогда не нужно и в данном контексте всегда является ошибкой. А посему можно принимать Spell по значению, и в этом случае бремя доказательства наличия заклинания лежит на вызывающей стороне. (В реальном коде принимали скорее по &&-ссылке, но ссылка также не может быть null). Имеем:
Раз - и первый if ушёл. Бонусом получили вызов методов через точку вместо стрелочки, а ещё из-за требования предоставить оператор сравнения проявили тот факт, что сравниваются указатели на заклинания вместо самих заклинаний. Валидным такое поведение будет являться только в том случае, если мы все заклинания интернируем.
Следующий if - это вызов isValid. Сам факт наличия такого метода является ошибкой дизайна. Именно, как так получилось, что у нас есть тип Spell, который может содержать что-то, что не является заклинанием? Возможность создать невалидное заклинание означает, что валидность нужно проверять снова и снова, и вызывающий код должен эти ошибки обрабатывать, вне зависимости от того, возвращаются ли они через коды возврата или исключения. Проверку валидности нужно переместить туда, где ей самое место: в конструктор Spell:
Следующий if - это проверка на наличие иммунитета к заклинанию. Как пишет автор:
Несчастные три строчки внизу — реальная работа метода. Остальное — проверки, можно ли совершить эту работу.
С чем я не согласен, поскольку проверка на наличие иммунитета - на мой взгляд, полноправная и важная часть логики. Но продолжим.
Следующий if проверяет, есть ли заклинание в наборе уже применённых, и делает по этому условию возврат, если оно уже есть. В противном случае заклинание добавляется. Иными словами, набор заклинаний уникален. А знаете, какая есть структура данных, которая поддерживает этот инвариант? Множество! Более того, эта структура данных имеет меньшую асимптотику для поиска значения, чем вектор, что может стать более эффективным, когда число заклинаний вырастет до пары тысяч или около того.
Что ж, заменим std::vector на std::unordered_set:
auto [appliedSpell, inserted] = appliedSpells.insert(spell);
if (!inserted)
{
return "Spell already applied";
}
applyEffects(appliedSpell->getEffects());
return "Spell applied";
Разумеется, это потребует написать хешер. Но мы можем просто делегировать это хешу от имени заклинания:
std::string applySpell(Spell spell)
{
if (this->isImmuneToSpell(spell))
{
return "Immune to spell";
}
auto [appliedSpell, inserted] = appliedSpells.insert(spell);
if (!inserted)
{
return "Spell already applied";
}
applyEffects(appliedSpell->getEffects());
return "Spell applied";
}
Осталось только два if-а, оба нужны для логики метода. Может ли тут пригодиться краткая запись для early return? Да, но, на мой личный взгляд, тут проблема стоит уже не так остро, особенно с учётом того, как похудел метод.
В общем, простите, автор, но в необходимости наличия краткой записи early return вы меня не особо убедили.
Насколько мне известно, Rust создавался под впечатлением (в частности) от OCaml непосредственно, и на OCaml даже была написана первая версия компилятора. Зачем вы его называете "старшим ML-братом Haskell" - неясно, в обоих языках есть фичи, которых нет в другом.
А вот про это можно поподробнее? В чём уступает? Мне на ум только меньший охват целевых платформ приходит.
Всегда можно понизить уровень сложности
А я вот прошёл Doom Eternal первым и потом прошёл Doom 2016 через силу. Потому что в плане геймплея Doom 2016 уступает почти по всем аспектам, сюжет более невнятный, а одинаковая оранжевая гамма всех уровней надоедает
Вот только ничего из этого в Hare нет.
Автора почему-то тотально удалили с Хабра. Вот ссылка на копию в web archive
Нет, зачем нужны аргументы со значениями по умолчанию - понятно, это много где есть. Зачем докидывать undefined, если аргументов недостаточно - неясно. Недостаточное количество аргументов при вызове - это ошибка на вызывающей стороне, а поведение JavaScript способствует тому, чтобы эти ошибки обнаруживались позже, чем надо.
А можете рассказать, как это на практике помогает в разработке?
Если вы пишите на Javascript код, для которого специально построенный для вывода типов инструмент не может вывести типы, то с чего вы считаете, что эти типы будут ясны человеку, который этот код считает?
Угу, вот только:
в других языках программирования map принимает передаёт в отображающую функцию лишь один аргумент - текущий элемент.
в Javascript недостаточное количество аргументов при вызове не вызывает ошибку, а докидывает undefined.
Зачем это сделано и как это помогает разработчику - вопрос открытый
Фича для написания багов, ага
Или, что гораздо вероятнее, ту переписывал однопоточный код на многопоточный и упустил все места, где доступ осуществляется из нескольких потоков.
Без статического анализатора, который проверяет следование этим правилам, толку от них довольно мало.
Коллега, вы делаете мне больно своим кодом. Можно же без аллокации вектора
char
-ов и без лишней мутабельности:А если конструктор по умолчанию дорогой?
Моё C++-кунг-фу недостаточно сильно. Я не разобрался, как исполнить какой-то код до вызова делегирующего конструктора.
Более приближенный к реальности пример был бы убедительнее.
Пожалейте сову. Object в Java тоже можно сравнивать на (не)равенство. Это не значит, что на Object есть арифметика.
Так а арифметика указателей тут причём? Указатель может быть невалидным и при этом не быть null
Давайте, и я вам покажу, что дело не в отсутствии ранних возвратов, а в кривой архитектуре. А чтобы разговор был более конкретным - напишу минимальный дополнительный код, чтобы он компилировался:
Оригинальный код
Для начала, зачем вообще принимать
Spell
по указателю? Указатель может быть null, и это то, что нам никогда не нужно и в данном контексте всегда является ошибкой. А посему можно приниматьSpell
по значению, и в этом случае бремя доказательства наличия заклинания лежит на вызывающей стороне. (В реальном коде принимали скорее по&&
-ссылке, но ссылка также не может быть null). Имеем:Код без указателей
Раз - и первый if ушёл. Бонусом получили вызов методов через точку вместо стрелочки, а ещё из-за требования предоставить оператор сравнения проявили тот факт, что сравниваются указатели на заклинания вместо самих заклинаний. Валидным такое поведение будет являться только в том случае, если мы все заклинания интернируем.
Следующий if - это вызов
isValid
. Сам факт наличия такого метода является ошибкой дизайна. Именно, как так получилось, что у нас есть типSpell
, который может содержать что-то, что не является заклинанием? Возможность создать невалидное заклинание означает, что валидность нужно проверять снова и снова, и вызывающий код должен эти ошибки обрабатывать, вне зависимости от того, возвращаются ли они через коды возврата или исключения. Проверку валидности нужно переместить туда, где ей самое место: в конструкторSpell
:В этом случае вызывающая сторона или получает валидный
Spell
, или не получает никакогоSpell
.Новый код без второго if:
Код с валидацией в конструкторе
Следующий if - это проверка на наличие иммунитета к заклинанию. Как пишет автор:
С чем я не согласен, поскольку проверка на наличие иммунитета - на мой взгляд, полноправная и важная часть логики. Но продолжим.
Следующий if проверяет, есть ли заклинание в наборе уже применённых, и делает по этому условию возврат, если оно уже есть. В противном случае заклинание добавляется. Иными словами, набор заклинаний уникален. А знаете, какая есть структура данных, которая поддерживает этот инвариант? Множество! Более того, эта структура данных имеет меньшую асимптотику для поиска значения, чем вектор, что может стать более эффективным, когда число заклинаний вырастет до пары тысяч или около того.
Что ж, заменим
std::vector
наstd::unordered_set
:Разумеется, это потребует написать хешер. Но мы можем просто делегировать это хешу от имени заклинания:
Новый код:
Финальная версия
Покажу отдельно итоговый
applySpell
:Осталось только два if-а, оба нужны для логики метода. Может ли тут пригодиться краткая запись для early return? Да, но, на мой личный взгляд, тут проблема стоит уже не так остро, особенно с учётом того, как похудел метод.
В общем, простите, автор, но в необходимости наличия краткой записи early return вы меня не особо убедили.
Насколько мне известно, Rust создавался под впечатлением (в частности) от OCaml непосредственно, и на OCaml даже была написана первая версия компилятора. Зачем вы его называете "старшим ML-братом Haskell" - неясно, в обоих языках есть фичи, которых нет в другом.