Комментарии 737
С нетерпением жду брекинг-новостей о массовых самовыпилах с трансляциями последних истерик в ТикТоке (а кто-нибудь еще помнит снапчат, этот моднючий блидинг-эдж 2хлетней давности?) и привычной уже констатации худшего года в истории человечества из-за необратимого повсеместного распространения моржового оператора в Питоне.
Так же с удовольствием бы почитал о токсичности формы ручек для отверток, например.
Если бы программы не читались после написания и не писались командой, то вопрос командного взаимодействия не стоял бы вовсе.
Но увы, делать эту работу в коммерческой разработке иногда приходится, а следовательно код становится одним из способов общения команды между собой.
Если исходный код выглядит как стенография лекции по истории гностических ересей в Южной Франции на палимпсесте конспекта курса по топологии, это не будет способствовать популярности такого ЯП на рынке труда: работодатели будут считать найм разработчиков такого кода слишком дорогим, а рабочих мест для работников не будет хватать.
С другой стороны, синтаксис 1С, не то, чтобы ужасен, конечно, нет — он чудовищен… А проблем таких на рынке нет.
Клингонский, по легенде, произошел из мертвого индейского языка, использовавшегося армейскими разведчиками США. Факт в том, что такие языки действительно использовались армейской разведкой: как раз для общения команды.
Тут налицо классическая дилемма: шашечки или ехать. К сожалению, молодые-перспективные разработчики 21 года 21 века настолько заигрались в шашечки, будь то новомодные новоязы, фреймворки, пайплайны, проценты покрытия тестами, прочие аджайлы или — омг — рейтинги токсичности синтаксиса, считая их чем-то действительно важным, что ехать уже становится проблематичным.
Почему молодые? Думаю, хейтеры синтаксиса Rust на Opennet вполне себе бородаты.
И да, само понятие «хейтер синтаксиса» вообще слабо коррелирует с опытом и мудростью, имхо.
Думаю, хейтеры синтаксиса Rust на Opennet вполне себе бородаты.
Ну я бородат. И мне не зашел синтаксис Rust. Правда на OpenNet я ничего не комментирую. Обстановочка не та.
Впрочем, я и не хейтер. Вопрос в том, что любой синтаксис становится привычным при регулярном использовании. Не в синтаксисе дело. Он явно создан сторонниками Pascal'я. Не хорош и не плох. Я поплевался пока пробовал Rust в деле, но явного отторжения не вызвал. Часть на С выглядит лучше, часть на Rust'е. Меня больше python выбешивал. Пока не плюнул и не сказал — не мое. И то ругаться не буду. Просто это не мой инструмент.
Вопросы к Rust они о другом. Впрочем, вот допишу статью потом обсудим… Лежит в черновиках…
И не только на opennet, a вот я вчера побрился. И кстати как раз сегодня перед тем как спать пойти (после того как релиз залил), размышлял на тему об отсуствии статей на хабре об уродливости синтаксиса раста.
Целая куча замечательных, полезных и интересных идей так заштукатурена отвратительным уродливым синтаксисом, что на код даже смотреть не хочется. Дело не в сложности чтения этого кода, а в отвращении которое он вызывает при одном только взгляде.
Это что-то на уровне животных инстинктов, совершенно не рациональное, но это есть. Есть совершенно неполиткоректный факт, — многим людям сложно смотреть на калек и деформированных людей. Они даже взгляд отводят. Со временем это проходит, какая-то привычка вырабатывается. Так вот rust такое-же «биологически обусловленное» отторжение вызывает…
Это скорее узость кругозора. Когда-то и меня сильно волновал вопрос синтаксиса: отступы или скобочки, как эти скобочки расставлять, int* p
или int *p
и прочие "чрезвычайно важные нюансы". С опытом понимаешь, что всё это ерунда. Только опыт желательно иметь разноплановый. Скажем, пощупать лисп с хаскелем, а не только на С десять лет писать.
Да ну? hard_tabs = true. Пробелы по дефолту, но и всё. У нас например половина настроек форматирования отлична от дефолтов так-то.
Эта проблема, на самом деле, перестаёт выглядеть надуманной, когда открываешь код в редакторе с альтернативными настройками на количество пробелов. Типа какого-нибудь mcedit (а бывает, что приходится).
Табы плохой способ форматирования, хоть и терпимый.
Смешанный стиль — печаль...
А вы можете сформулировать более четкие претензии к синтаксису? Я понимаю ваши чувства, но не зная их подоплеку сложно спорить конструктивно.
Там вон уже и минусов понаставили, и в узости кругозора обвинили, а на самом деле это возможно из разряда фобий, или просто нечто социо-культурное. Вот как пример www.quora.com/Why-do-some-people-dislike-people-with-disabilities попытка объяснения. Но явление довольно массовое.
По вашему синтаксис языка программирования не достоин обсуждения?
А предметное обсуждение особенностей языков программирования — это круто и очень полезно.
А красный флаг тогда тут при чем? Мы разве гонки обсуждаем, или мореплавание, или революционную борьбу?
Абсолютно согласен.
Вообще, обсуждение характеристик синтаксиса (даже если называть их уместными словами с соответствующими значениями, типа «читаемость») — мне непонятно в принципе. Совершенно очевидно, что любой язык (да, и кобол, и сиквел — тоже) превращается из нечитаемого в читаемый по мере погружения.
Когда я еду во Францию, я освежаю в голове всякие «сильвупле» и «мерси», а не говорю, мол, «фу, какой нечитаемый язык, я лучше пару дней буду питаться не в кабаках, а в супермаркетах без кассиров».
Какая вообще нахрен разница, каков синтаксис языка, если он не умеет, например, из коробки задействовать все ядра? А если умеет — ну в чужой монастырь не надо со своим синтаксисом лезть, можно потратить пару часиков на привыкание.
Какая вообще нахрен разница, каков синтаксис языка, если он не умеет, например, из коробки задействовать все ядра? А если умеет — ну в чужой монастырь не надо со своим синтаксисом лезть, можно потратить пару часиков на привыкание.
Пользователю языка — без разницы.
Разработчикам новых языков — полезно учиться на чужих ошибках, чтобы их не повторять.
Разработчикам новых языков — полезно учиться на чужих ошибках, чтобы их не повторять.
Это было бы круто, кабы существовала непротиворечивая корректная шкала оценки синтаксиса. А так, как ее нет, то и определить — ошибка ли это, или суперспособность — возможным не представляется. «Демократичным» голосованием это определять? — Увольте.
Вон там внизу предлагают изменять язык до неузнаваемости, развивать и дополнять, пока количество синтаксического сахара не превысит количество символов на клавиатуре (а потом и в юникоде) — я бы лично не хотел, чтобы над языками издевались таким способом, выбранным по результатам голосования у джаваскриптовиков, которые желают, чтобы все мучались так, как они.
Я лучше уж разочек синтаксис (любой) освою, а потом буду языком пользоваться, а не переучивать семантику каждые полгода.
Ну почему же не надо.
Синтаксис языка — не что-то, высеченное в камне, его можно и нужно менять, развивать и дополнять. Разумеется, после релиза сложновато депрекейтить, но уж дополнять то, в том числе другой синтаксис для существующих вещей можно.
Ну и да — это может быть полезно для других языков
Совершенно очевидно, что любой язык (да, и кобол, и сиквел — тоже) превращается из нечитаемого в читаемый по мере погружения.
Сложный декодинг определений типов C — как раз пример, что погружение помогает привыкнуть к тому, что "тут так принято" и смириться, но не тому, что всё равно придётся тратить каждый раз заметную порцию интеллектуальных усилий на его раскрутку.
С теми же фигурными скобками и фальшивым отступом аналогичный пример.
За что я люблю синтетические примеры, так это за всё.
int (*fns[16])(int * const * p)
Я готов поспорить, что когнитивные усилия здесь придется тратить на понимание того, как этот тип заполняется, и зачем вообще автору втемяшилось объявлять такую функцию. После того, как это будет понято, тип сам собой нарисуется.
В простом коде все эти фигурные скобки и отступы считываются мгновенно, а в сложном — основные проблемы чтения связаны далеко не с ними.
Синтаксис ЯП может быть презрительным?
Вполне себе синтаксис можно презирать.
"Токсичность" можно вольно транслировать как "недружелюбность". И недружелюбность чего-бы-то-ни-было, синтаксиса или ещё чего вполне возможна.
С первого дня изучения раста меня напрягали только лайфтаймы, всё остальное, плюс минус, уже видел в крестах.
Так же считаю ваши аргументы за первые 2 пункта неудачными и протянутыми за уши, но и сами претензии, имхо, крайне глупые.
ВЖ пугают с непривычки. Когда приходит понимание, что их синтаксис не просто так аналогичен дженерикам, становится совсем просто.
Даже любопытно стало. А примера неочевидности под рукой не сохранилось?
Ну есть более-менее очевидные например асинки. А есть менее очевидные, например:
trait SomeTrait {
fn foo(value: &i32) -> Self;
}
struct Bar<'a>(&'a i32);
impl<'a> SomeTrait for Bar<'a>
{
fn foo<'b>(value: &'b i32) -> Bar<'b> {
Bar(value)
}
}
То есть претензия компилятора понятна, но не очень ясно что в таком случае нужно делать. Намерение вроде понятно: нам приходит на вход ссылка и мы хотим вернуть себя, ограниченным тем лайфтаймом который пришел на вход. Трейт SomeTrait
у нас приходит из библиотеки, и менять его мы не имеем права. Вот как такую ситуацию можно разрешить?
Можно было бы попытаться сделать такую сигнатуру:
impl<'a> SomeTrait for Bar<'a>
{
fn foo<'b: 'a>(value: &'b i32) -> Bar<'b> {
Bar(value)
}
}
Но компилятор не разрешит, скажет что оригинальная сигнатура другая:
error[E0195]: lifetime parameters or bounds on method `foo` do not match the trait declaration
--> src/lib.rs:9:11
|
2 | fn foo(value: &i32) -> Self;
| - lifetimes in impl do not match this method in trait
...
9 | fn foo<'b: 'a>(value: &'b i32) -> Bar<'b> {
| ^^^^^^^^ lifetimes do not match method in trait
error: aborting due to previous error
Хотя в теории мы просто сделали дополнительное ограничение. То есть в нашем случае лайфтаймы оказывается не контравариантные, и если создатель библиотеки не предусмотрел такого вариант то упс.
Ну и решение: к сожалению, без изменения типажа решения не выйдет. Решается так:
trait SomeTrait<'a> {// Обратите внимание на время жизни, добавленное в типаж
fn foo(value: &'a i32) -> Self;
}
struct Bar<'a>(&'a i32);
impl<'a> SomeTrait<'a> for Bar<'a>
{
fn foo(value: &'a i32) -> Bar<'a> {
Bar(value)
}
}
Еще один пример — стандартный типаж Index
, возвращающий ссылку на свои внутренности. Из-за этого невозможно возвращать только что сконструированные структуры-обертки над частью своих внутренностей и это иногда зарубает интересные идеи API.
Ну вот речь о том, что типаж нельзя поменять (в реальной задачи это actix_web::FromRequest
), форкать или патчить либу ради этого как-то не очень хочется.
Так что проблемы с лайфтаймами бывают вполне себе. Казалось бы, зачем трейту знать про то что реализация хочет лайфтайм как-то именовать и что-то на основании этого делать? Да вроде незачем. А оказывается, что без этого нифига не работает.
По сути это обычный C-подобный синтаксис, который имеют и многие другие языки, но с различными современными улучшениями.
Может я что-то упустил, но Rust имеет ML-подобный синтаксис. Очень велико влияние OCaml, на котором он изначально и писался.
К сожалению оказалось, что от него совсем не просто избавиться: конструкция A<B>() выглядела бы логичнее и лаконичнее, но в контексте выражения парсер не может отличить ее начало от операции сравнения A < B. Отсюда и возникает необходимость дополнить угловую скобку разделителем сегментов в пути ::.
А если не перегружать символы
<
и >
, и вместо этого писать A[B]()
?Тогда и токсичных рыб нет, и метафора «из множества реализаций
A
берём соответствующую B
» совершенно прозрачная.Квадратные скобки заняты для слайсов и массивов.
Да, но синтаксически содержимое квадратных скобок в каждом случае разное.
Эти круглые скобки встречаются только в разных контекстах, в отличии от обсуждаемых квадратных.
А проблемы возникают самые разные. Например, Rust обходится без предварительного объявления рекурсивных типов или функций (просто нет такой возможности, потому что не нужно), а в C++ их сначала нужно декларировать, а потом объявлять (кстати, это куда более глубокие вещи, чем лежащие на поверхности фигурные скобочки, которые тут обсуждают).
А в этом случае у парсера уже есть подсказка, что-есть-что в виде ключевого слова fn
. И опять не нужно знать извне, что мы парсим — объявление функции или арифметическое выражение. Эта информация уже закодирована в разбираемом тексте. Если выкинуть fn
, как тут выше уже предлагали, то это становится невозможно (не говоря уже о том, что эти люди вообще что ли не пытались никогда ничего искать по коду на GitHub.com?)
То есть у вас А::<В>(), то есть турбофиш заменится на A[B](), окей.
А теперь представьте себе массив А, в котором лежат указатели на функции, и вы используете имя счетчика В. Тогда выглядеть это будет: А[В](). Упс…
Вас ведь не смущает, что для поиска в HashMap по ключу используются те же самые квадратные скобки, но семантика у них совсем другая, чем при индексировании массива?
В верхнем комментарии треда я указал, что оба названных вами случая обобщаются как «из набора функций, обозначенного A, взять одну конкретную, соответствующую B», так что схожесть синтаксиса для этих двух случаев совершенно намеренная.
Доступ к элементу массива по индексу и к значению в хэш-таблице по ключу осуществляется во время выполнения и вообще говоря для этого используется один и тот же оператор (собственные реализации типажа Index
). Именно Index
управляет возможностью делать доступ [i]
в рантайме, это просто сахар для вызова container.index(i)
, однако в случае обобщенной функции требуется совершенно другая семантика — выбор функции осуществляется на этапе компиляции.
а разница между сложением указателей и поиском в хеш-таблице — не должна быть отражена в синтаксисе.
А почему должна? И то и то специально построено на абстракции типажа Index
— поэтому использование и не должно отличаться.
Грубо говоря, что при индексации массива производится прибавление к указателю массива смещения — деталь реализации. Могли бы и массивы реализовать под капотом, как хеш-таблицы (если бы спецификация языка это позволяла).
А способ вызова функции напрямую влияет на то, что будет выполняться — или точно определенный код, или относительно произвольный
А почему должна?
Я о том и говорю, что ни та ни другая не должна; что подстановка чего бы то ни было во время компиляции — деталь реализации, неинтересная пользователю языка.
Rust может и значения элементов массива подставлять во время компиляции, несмотря на [].
А способ вызова функции напрямую влияет на то, что будет выполняться — или точно определенный код, или относительно произвольный
Я о том и говорю, что
A[B]()
— вызов «относительно произвольного» кода, хоть A массив, хоть хеш-таблица, хоть дженерик.На деле же получается не очень удобно.
Можете рассказать, какие с этим возникают проблемы?
Но я не люблю синтаксис, завязанный на чуткое понимание контекста. То есть, когда есть миллион использований одних и тех же символов и в зависимости от контекста у них разное значение.
Хочется, чтобы контекст «берем N-ый элемент» вычитывался сразу, а не путался с «вызываем функцию от N».
Поскольку редактор в самом Матлабе не совершенствуется уже кучу лет и подсветка и навигация по коду остаются достаточно примитивными, если хочется прочитать и вникнуть в кусок кода, то такие случаи с круглыми скобочками могут приводить к тому, что придется прыгать по всему скрипту.
А еще проще: мне просто не нравится и неудобно. Я писал на Матлабе большую часть своей программерской жизни, как и на Си. И все еще не нахожу это удобным.
Банальный пример, который часто бесит.
Есть функция f
, принимающая число и возвращающая масив. Вам нужен второй элемент. В нормальном (читай, любом) языке вы напишите res = f(10)[2]
(нумерация с единицы), но в Матлабе будет res = f(10)(2)
. На этом этапе парсер вам скажет "какие нахрен скобки за скобками? Я не понимаю". И приходится out = f(10); res = out(2)
, или [~, res] = f(10)
. Со вторым, однако, проблема, потому что количество аргументов в таком матчинге может влиять на алгоритм и вывод функции — то есть не всегда применимо, но это уже немножко другая история.
И парсер тут не тупой: функция ведь может возвращать функцию, и что тогда делать? В какой байт-код компилить?
Но самое паршивое, это когда читаешь код плохого прогера (а МАТЛАБ — система для инженеров). И пойди тогда разберись, что такое ar(i, k)
— индексация массива или вызов функции.
Буквальный код ошибки выглядит так
Error: Indexing with parentheses '()' must appear as the last operation of a valid indexing expression.
Indexing with parentheses '()' к тому, что индексирование тут ещё бывает через фигурные скобки для специальных массивов.
Если столько не исправляют, то, скорее всего, что-то фундаментальное. В принципе, я согласен с вами в том, что причина, скорее всего, не в "функция ведь может возвращать функцию, и что тогда делать?".
Скорее всего это фундаментально положенный болт.
Не думаю, хотя не сильно удивлюсь, если и вправду так.
А я уверен, что именно в этом и дело. Это общая беда любых языков программирования, которые идут "в нагрузку" к другому программному продукту.
Расчёт тут простой: если у всего продукта целиком недостаточно фич — язык его не спасёт, а если достаточно — то пользователи никуда не денутся. Поэтому сам язык развивается по остаточному принципу, лишь бы на нём хотя бы в теории можно было писать. Это и есть фундаментально положенный болт.
A[B]
это еще доступ по индексу.
В статье же объяснялось что синтаксис такой, что вам не нужно знать, что такое A
и что такое B
, чтобы правильно распарсить исходник (т.е. вы не знаете, что это имя функции, так что ваше знание "Имя функции не может быть ни массивом, ни слайсом." вам ничем не поможет). И это хорошо — это позволяет всегда корректно парсить произвольные куски кода, что очень хорошо для подсветки синтаксиса и извлечения информации из еще недописанного кода, чтобы IDE могла чем-то помочь, а не свешивать лапки.
::<>
я не вижу:- Какую информацию сейчас может извлечь IDE из ещё недописанного кода, пока функция слева от рыбы ещё не определена? Какая информация потерялась бы при переходе к
[]
? - Как допущение имени типа внутри
[]
мешает подсветке синтаксиса?
Проблема, которую я вижу — что если внутри
[]
будет тип вместо выражения или наоборот, то это отловится не сразу при парсинге, а на более позднем этапе. Но и здесь ничего нового: сейчас неподходящий индекс в let hello = [42]; print!("{}", hello["world"]);
тоже ловится не сразу при парсинге, а на более позднем этапе.Какую информацию сейчас может извлечь IDE из ещё недописанного кода, пока функция слева от рыбы ещё не определена
IDE уже знает, что это функция или тип и может просматривать свою базу данных о функциях и типах. В вашем варианте она должна еще предлагать и варианты из выражений.
Написав x::<
уже известно, что x
или имя функции, или имя типа, а дописав x::<T>(
уже понятно, что точно имя функции. Основываясь на этом IDE может искать x
в своей базе данных функций или типов и предлагать, что делать дальше. Конечно, если такой функции/типа еще не написано, то она ничем не поможет, но если есть — то уже поможет.
Если использовать синтаксис с квадратными скобками, то написав x[
непонятно, что такое x
— имя функции? имя типа? слайс?
Написав x[T](
это все еще неясно — x
это слайс или имя функции.
А если в области видимости окажется и слайс с именем x
и имя функции или имя типа с именем x
? Тут IDE предлагать адекватные варианты уже сложнее — если она сможет понять, что именно ожидается в месте написания кода, то предложит, а если нет — то в лучшем случае будут лишние варианты а в худшем — только неправильные.
Проблема, которую я вижу — что если внутри []
будет тип вместо выражения или наоборот, то это отловится не сразу при парсинге, а на более позднем этапе.
Есть еще проблема. В Rust можно инстанцировать Unit-структуры просто написав их имя:
struct Unit;
// x имеет тип Unit и содержит единственное значение Unit этого типа
let x = Unit;
x[Unit]
Это индексация выражения x
только что созданным экземпляром типа Unit
или инстанцирование generic с типом Unit
?
Зависело бы от того, что такое x
.
Ведь это те мелочи, от которых любит подгорать у хейтеров C++, когда изменения в одном месте программы внезапно могут сказаться на другом ее месте (в данном случае добавление строчки с определением x
выше, которое скроет начальное определение x
).
По-моему хорошо, что создатели языка постарались учесть эти проблемы и уменьшить их, насколько возможно. То, что код делает, должно быть минимально ясно из взгляда на локальный кусок кода, а не ворошением всего файла/модуля/проекта. Может быть и у Rust это не всегда получается, я не знаю, но это же не повод совсем на это забить
Конечно, если такой функции/типа еще не написано, то она ничем не поможет, но если есть — то уже поможет. Если использовать синтаксис с квадратными скобками, то написав x[ непонятно, что такое x — имя функции? имя типа? слайс?
Я о том и говорю, что пока x не определено — IDE не поможет ни при том ни при другом синтаксисе, а как только x определено — IDE известно, что это такое, и при том и при другом синтаксисе.
А если в области видимости окажется и слайс с именем xи имя функции или имя типа с именем x?
То это ошибка «E0428: the name is defined multiple times» хоть при том, хоть при другом синтаксисе.
В Rust можно инстанцировать Unit-структуры просто написав их имя
А зачем их как-то отдельно ещё и инстанцировать? Вообще сомнительная фича.
Затем, что они становятся бесполезными, если не иметь возможности их инстанциировать.
Они сами себе инстансы же.
Нет, их нужно инстанциировать, если необходимо получить объект данного типа. Как и другие структурные типы.
Ну например Result<(), Error>
(функции типа записи в файл) требуют в конце Ok(())
чтобы компилировалось. Без инстанса ()
типа ()
тут не обойтись
Все "инстансы" такого типа будут равны друг другу, так что нет никакого смысла "создавать" инстансы. Достаточно использовать имя типа в качестве алиаса для синглтона инстанса.
Стандартный ничем не отличается от энума enum MyUnit { Unit }
. Наделять его какими-то специальными свойствами "просто шобы було" нет никакого смысла — и так работает, зачем вкоряживать кастомную логику? Так-то кому-то массив юнитов может показаться бесполезным, вещь которую нужно запретить, но в дальнейшем оказывается что всё несколько сложнее.
Ну, не совсем, unit-like структуры чаще используются в качестве тИповых маркеров, а не явно в коде.
Во-первых, конечно, любому современному сложному языку программирования не хватает спецсимволов. Тех же скобок, например, чтобы не использовать знаки «больше» и «меньше» в качестве скобок. Да, есть целый Unicode, но нужно чтобы эти символы были на всех клавиатурах мира — а значит, мы опять возвращаемся к ASCII. Наверное, неплохим решением было бы, если бы консорциум Unicode выделил 20 наиболее важных символов и предложил бы наносить их на все клавиатуры на буквенные клавиши (возможно потребовался бы еще один Shift). Для полной совместимости с ASCII можно было бы даже
продублировать их в начале ASCII, в кодах 0x01..0x1F (исключая POSIX-символы переносов строки, табуляций и чего-то там еще). Но такое чудо вряд ли случится:)
Фигурные скобки и точка с запятой нужны. Это именно то, что позволяет структурировать код явно, не надеясь на невидимые пробелы и табы, как это принято в Python, а также make-файлах и еще каких-то древних форматах.
Постфиксная запись типа… ну лично мне не нравится. Но наверное авторы правы. Смысл var и let я понимаю, ключевые слова, предваряющие введение новых имен, должны быть обязательно.
Сокращения в ключевых словах это хорошо, особенно fn. Это лучше чем функции в C/C++ без ключевых слов вообще. Значительно упрощает работу парсера. А вот стрелки в сигнаруте функций… в Go вообще обошлись без спецсимволов там. Просто возвращаемый тип после скобок с аргументами.
Замыкания — нет, не нравятся. Я бы вообще не стал делить функции на обычные и замыкания, использовал бы везде fn как универсальное ключевое слово. Кстати, в Rust вроде бы нет явного связывания переменных, как в С++ (в квадратных скобках).
Также не нравится применение символа "|" в паттерн-матчинге. Это же всегда было «Битовое ИЛИ», зачем его мешать сюда? Чем запятая не угодила?
match x {
1 | 2 => println!("one or two"), // ну и что это такое? "Битовое или" или несколько аргументов матчинга?
_ => println!("anything"),
}
С использованием :: наверное авторы правы, хотя мне не нравится. Опять же — недостаточно спецсимволов!
Использование угловых скобок — с одной стороны наверное это выразительно, но всем известно про то что они путаются с символами «меньше» и «больше», и для парсера это тоже может быть проблемой. Опять же нехватка спецсимволов.
Для «цитирования» кода в макросах тоже не помешали бы особые, уникальные скобки-кавычки. Ну и т.д.
Совместимость с ASCII в век победившего UTF-8 уж точно не нужна. Вот в раскладку клавиатуры новые скобки по-включать — идея по-лучше, жаль только нереализуема.
А первые 32 байта ASCII как раз не используются, кроме нуля, пробела, переносов строк и табов. Совместимость с древними телетайпами вряд ли кому нужна.
Ну, Windows API вместо этого UTF-16 использует. Невелика разница.
Однако, код программы напрямую в Windows API никто передавать не будет, а все нормальные текстовые редакторы UTF-8 таки поддерживают.
Уточнение: поддерживать-то поддерживает, но баг в conhost до сих пор не закрыли. ReadFile из стандартного ввода не может прочитать русские буквы из консоли.
Потому что ReadFile заполняет буфер байтами, а не символами. Задача программиста конвертировать массив байтов в символы и строку.
В том-то и дело что там конвертировать просто нечего, ReadFile читает ерунду вместо строки в кодировке utf-8.
Нет конечно. ReadFile будет отдавать байты в том виде как их отправила другая сторона, без конвертации из одной кодировки в другую.
Другая сторона тут — conhost. И в нём есть баг, из-за которого он при активной странице 65001 пишет вместо русских букв в кодировке UTF-8 какую-то ерунду.
А программа которая для conhost поставляет данные, она знает, что должна конвертировать строку в байты в кодировке 65001 или по‐прежнему отдаёт их в 1251 и 866?
Нет никакой "программы которая для conhost поставляет данные", conhost как раз их и поставляет. По крайней мере, для стандартных консольных окон это верно.
Разумеется, conhost всё знает, и любые кодовые страницы кроме 65001 работают прекрасно. И 65001 тоже работала бы прекрасно если бы conhost выделял буфер по-больше. Где-то я читал, что он тупо не может преобразовать 1 символ в 3 байта потому что ему не хватает места.
Если языки программирования массово будут использовать символы Юникода (как это уже делает Raku), то подтянутся и производители клавиатур.
Во-первых, конечно, любому современному сложному языку программирования не хватает спецсимволов.
Ох нет спасибо: я только сегодня возился с кодом на Xtend, в котором как часть синтаксиса используются ёлочки
«»
. Часть исходников была в UTF-8, часть — в CP-1252, и используемая кодировка в самом файле никак не обозначена — «догадайся мол сама».Но кстати, какие у них интересные есть символы — полускобочки двух видов, еще одни «панциреобразные» скобочки ❲ ❳
В принципе, такие бы всем непомешали, соответственно этим ребятам свободных клавиш понадобилось бы меньше:)
Синтаксис с вертикальной чертой в качестве "или" довольно распространен в функциональных языках, откуда паттерн-матчинг и завезли в Rust.
Все-таки Rust — это императивный язык, поэтому сравнение с Haskell в этом отношении немного неуместно. Например, в Haskell нет традиционного для императивных языков условия if
без else
, а в Rust оно есть. Ну и там, где возникает в Haskell необходимость писать императивно, появляются и фигурные скобки, и точка с запятой. Хоть опционально, но ведь почему-то они в языке присутствуют, не так ли?
Нет, это визуальный мусор. Фигурные скобки и точки с запятой для структурирования не нужны ни в хаскеле, ни в идрисе, ни в агде, и у меня ни разу из-за этого не было проблем
Мне напомнить, зачем в GHC ввели расширение EmptyCase?
любому современному сложному языку программирования не хватает спецсимволов.
Свободных спецсимволов или комбинаций полно, просто разработчикам языков немного мешает уже накопленный опыт.
Тех же скобок, например, чтобы не использовать знаки «больше» и «меньше» в качестве скобок
Мне в этом плане нравится, как это сделано в D. Зачем пытаться копировать неудачный синтаксис из C++ и усложнять парсинг, когда можно использовать любой другой символ: !
, @
, #
, $
, означающий, что дальше идёт набор шаблонных параметров:
Dictionary![TKey, Tuple![Item1, Item2]]
или Dictionary!(TKey, Tuple!(Item1, Item2))
Также не нравится применение символа "|" в паттерн-матчинге. Это же всегда было «Битовое ИЛИ», зачем его мешать сюда? Чем запятая не угодила?
А вот тут мне нравится, как это сделано в C# — через добавление новых ключевых слов, а не усложнение логики старых:
x switch
{
1 or 2 => ...,
>3 and <6 => ...,
_ => ...,
}
#[test]
fn test_foo() {
/* ... */
}
А в C# сделано неправильно. Там есть оператор switch, его и нужно было расширять — разрешить ему возвращать значения, расширить синтаксис case-образцов и всё остальное, а они вместо этого взяли и ввели альтернативный синтаксис, совершенно ни на что не похожий. В конце концов, можно было бы кроме switch добавить еще match, в котором не требуется break после каждого case; но в остальном и switch и match должны обладать одинаковыми возможностями. Это логично, подобно тому как скажем циклы while и do..while существуют одновременно и отличаются только в одном аспекте.
возможно потребовался бы еще один ShiftЭта проблема уже давно решена в странах, где пишут на польском и других странных языках с лишними буквами. Кстати, спецсимволов у них тоже там больше зачастую.
Запятая будет лишним гемором для парсера: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=84d9b5daaacbb00406e433c389ecb869
Однако на самом деле, предоставляемые Rust возможности требуют явного синтаксического разделения статических обращений xx::yy::zz и динамических xx.yy.zz, где результат будет зависеть от значения объектов xx, yy и zz во время выполнения.
Второе обращение явно заслуживает какого-то специального названия, но оно точно не "динамическое".
Программа, написанная на Rust, не имеет информации о типах во время выполнения, так как при компиляции происходит "стирание типов".
Не вижу никакого "стирания типов" при компиляции. Автор явно понимает под этим словом что-то иное, не совпадающее со значением этого слова в Java или TypeScript.
При желании — да, см. трейт Any. При большем желании можно и что-то более хитрое придумать.
Однако, я бы не сводил Type Erasure к возможности получить тип переменной во время исполнения. Ну или, если всё же принять ваше определение, всё равно остаётся непонятной следующая связка:
Программа, написанная на Rust, не имеет информации о типах во время выполнения, так как при компиляции происходит "стирание типов". Поэтому возникает необходимость различать динамические и статические обращения: механизм их работы сильно отличается, как и последствия, к которым они приводят
Нет, я решительно не понимаю как невозможность узнать тип переменной в рантайме влияет на необходимость разделять операторы ::
и .
. К примеру, в Java используется только точка и ничего (да, первый оператор тоже есть, но он означает иное).
На самом деле мне удалось нагуглить вот какую проблему в параметричности Rust:
In particular, the impl specialization RFC describes a language change that will allow the invocation of a polymorphic function f to end up in different sequences of code based solely on the concrete type of T, even when T has no trait bounds within its declaration in f.
Код: play.rust-lang.org/?gist=461340df78e7db710b6e37b592c1ad6e&version=nightly
Тут становится уже сложнее говорить про стирание типов.
Но мне тоже непонятно, причем тут разделение на операторы `::` и `.`.
При желании — да, см. трейт Any.
Any
не дает никакой информации о структуре типа, он позволяет лишь получить уникальное число-идентификатор типа. И то только в том случае, если данный трейт используется. Так-то при желании вы можете руками реализовать хранение информации о типе в рантайме, но ведь речь не о том, что можно сделать руками, а о том, как язык работает по умолчанию.
В Java насколько я знаю статические методы резолвятся в рантайме, также как и не статические. Статик там означает, что вызов относится к классу, а не то, что вызов будет определен на этапе компиляции, как в Rust.
А где говорилось, что должна быть какая-то информация о структуре типа? :-)
В Java насколько я знаю статические методы резолвятся в рантайме, также как и не статические.
Только если считать промежуточный байт-код "рантаймом". Так-то при генерации машинного кода вызовы статических, приватных и финальных методов превращаются в вызовы по фиксированным адресам (точно не знаю, но я не могу даже представить зачем делать иначе).
Статик там означает, что вызов относится к классу, а не то, что вызов будет определен на этапе компиляции, как в Rust.
Вы так пишете, как будто в Rust те вызовы, которые идут "через точку", не определены на этапе компиляции!
А где говорилось, что должна быть какая-то информация о структуре типа?
Вот здесь говорилось, что этой информации нет во время выполнения: "Программа, написанная на Rust, не имеет информации о типах во время выполнения, так как при компиляции происходит "стирание типов". ". Мне показалось, что вы спорили с этим утверждением.
Java-код компилируется в инструкции виртуальной машины и речь именно об этой компиляции. Во что транслируются уже эти инструкции и транслируются ли вообще, в контексте данного разговора не важно.
Вызов через точку можно рассматривать как вызов, при котором неявно передается указатель на предыдущий сегмент, а значение этого указателя определено в рантайме. Тогда как в вызове через ::
сегменты являются именами статических пространств имен и не имеют представления во время выполнения. Поэтому структуры в Rust не могут иметь статических полей, в отличии от классов Java, и статические методы в Rust — это просто функции, расположенные в пространстве имен структуры, ничего более.
А сколько информации требуется? Я вот считаю, что TypeId — это уже информация о типах. Вам зачем-то нужна информация о структуре. Кому-то ещё понадобится исходный код типа. В каждом случае получится разное понимание стирания типов. Где будем останавливаться и почему?
Java-код компилируется в инструкции виртуальной машины и речь именно об этой компиляции.
Но ведь если выкинуть детали реализации, то в таком случае всё ещё проще же: вызов статического метода приводит к инструкции invokestatic, после которой указывается вызываемый метод. Метод указывается компилятором статически. Где тут резолвинг в рантайме? :-)
Вызов через точку можно рассматривать как вызов, при котором неявно передается указатель на предыдущий сегмент, а значение этого указателя определено в рантайме.
Но метод-то связывается не по значению указателя, а по его типу (за исключением ситуации trait object, но эта фича используется относительно редко, а точка пишется всегда).
Поэтому структуры в Rust не могут иметь статических полей, в отличии от классов Java, и статические методы в Rust — это просто функции, расположенные в пространстве имен структуры, ничего более.
Не вижу отличия от Java.
Про скобки и отступы:
Из-за древнего примера из си предлагается ещё последующие 100 лет дуть на воду. Опять же не надо забывать, что в современности мало кто программирует в notepad/nano, а отступ в vscode будет виден очень хорошо, за все годы я вспомнил что сделал такую ошибку только один раз, и то всё было найдено и исправлено меньше чем за минуту.
Про блоки: опять же блоки нет никакой нужды обозначать именно скобками, от этого они не перестанут быть блоками в других языках. А вот про область видимости — тут это растёт больше из проблем borrow-checker'а, и в примере с локом, в других языках это выглядит менее вербозно и без борров-чекера, что-то типа:
withLock(mutex): ...
Про;. Опять же нет никаких проблем сделать синтаксис без этого в компилируемом языке, если выражение возвращает (), то не надо ставить;, а если что-то возвращает и находится в конце функции — то это и есть её результат. Просто Раст очень пытался перетянуть на себя си-аудиторию, вот и получилась эдакая микс всего сразу.
Читая про постфиксную запись, я скорее склонен считать, что это попытка выдать желаемое за действительное, можно написать точно такой же текст о том почему это в плюсах удобнее, например auto a = fb() сразу сообщает пользователю об автовыведение типа — удобно как бы. Т.е. не то что это действительно так, но довольно спорный вопрос как удобнее на самом деле.
Опять же let mut избыточен, достаточно mut. Рассуждения о том какой синтаксис чтобы парсеру было удобно — вообще странные.
Не согласен с автором в том, что понимание как работает язык делает синтаксис лучше, скорее просто замыливает глаз, так как описанное в статье скорее из разряда о том как выкручиваются авторы самого раста, а не о горошком синтаксисе. Не смотря что Ява основной язык в моём резюме, но написание на этом вербозном языке, даже спустя десяток лет, не вызывает никаких положительных эмоций, а раст даже Яву переплюнул, правда не только из-за синтаксиса.
PS: всех с Новым Годом!
Опять же не надо забывать, что в современности мало кто программирует в notepad/nano, а отступ в vscode будет виден очень хорошо
Как насчет code review через веб-интерфейс? Или публикации кусков кода в мессенджерах? Или копи-пасты со stackoverflow? Отступ-то вы увидите, но у вас не будет дополнительного контроля, что вы не сдвинули что-то где-то. На Rust также распространена практика, когда тебе нужно что-то быстро проверить на работоспособность в онлайн-плейграунде и копируешь туда код зачастую с "поехавшими" отступами, но благодаря скобкам об этом не нужно беспокоиться.
Хотя то, что отступы видны хуже, следует также из того факта, что люди с плохим зрением их не видят, в отличии от скобок.
в других языках это выглядит менее вербозно и без борров-чекера, что-то типа: withLock(mutex): ...
А если нужно лочить не одно значение, а сразу два или три? Городить вложенные блоки? И ведь речь не только о локах мьютекса, а вообще о всем, для чего удобно использовать RAII.
Опять же нет никаких проблем
fn foo() { .. }
fn bar() -> bool { .. }
let val = if x < 42 {
foo(x)
} else {
bar(x)
}
Какого типа должно быть значение val
?
Опять же let mut избыточен, достаточно mut
Нет никакого let mut
, есть let PATTERN
. let
— это не оператор объявления переменных, это оператор сопоставления (в паттерне переменных может и не быть).
А если нужно лочить не одно значение, а сразу два или три? Городить вложенные блоки? И ведь речь не только о локах мьютекса, а вообще о всем, для чего удобно использовать RAII.
Пример прямиком из MSDN:
using (StringReader left = new StringReader(numbers),
right = new StringReader(letters))
{
...
}
Ужас. Лучше бы в языке этого варианта не было!
withLock(mutex1, mutex2)
using (StringReader reader = new StringReader(numbers),
writer = new StringWriter(...))
{
...
}
Через запятую поддерживается определение переменных только одного типа, а в реальной жизни обычно используется что-то вроде
using (var reader = new StringReader(numbers))
using (var writer = new StringWriter(...))
{
...
}
Мне и так нравится, но возможно кому-то это покажется уродливым.
Не знаю как в C#, но в Java с try-with-resources
ответственность за корректное освобождение ресурсов перекладывается на вызывающую сторону. К тому же не всегда использование ресурсов настолько локализовано, что безошибочное использование try-with-resources
очевидно.
Использование Cleaner
улучшает ситуацию и избавляет от необходимости следить за освобождением в тривиальных случаях, но в более сложных — головной боли не избежать. О проблемах освобождения ресурсов в Java подробно рассказывается в докладе Евгения Козлова "Вы все еще используете finalize()? Тогда мы идем к вам".
люди с плохим зрением их не видят, в отличии от скобок.
При чтении текста не озвучиваются знаки препинания, а при посимвольном чтении озвучиваются в том числе и пробельные символы. Тут разве что предпочтительнее для отступов использовать табы, а не пробелы, чтобы не пересчитывать в уме число пробелов в число отступов.
Или копи-пасты со stackoverflow?
Перепечатайте, чай, руки не отвалятся.
В статьях PVS-studio до половины ошибок — это кривой копипаст: где переменную не переименовали, где + на — сменить забыли, где ещё что-то похожее.
Как насчет code review через веб-интерфейс? Или публикации кусков кода в мессенджерах?
А вот этого просто не понял. В чём там проблема?
Как насчет code review через веб-интерфейс?
Меня прежде всего интересует как оно в vscode, всё остальное вторично, если очень надо — надо в web-интерфейс запилить что-то типа плагина из vscode. Зачем дополнительно страдать ради тех кто копирует в чатах не могу понять, хотя большинство чатов уже нормально понимают
2+2
Хотя то, что отступы видны хуже, следует также из того факта, что люди с плохим зрением их не видят, в отличии от скобок.
Не видно отступы, при этом видно {}? странно, ну да ладно, так как я, человек с плохим зрением, не хочу их расставлять для кого-то кроме себя, а расставлять я их не хочу, хотя когда-то в плюсах я считал это обязательным.
А если нужно лочить не одно значение, а сразу два или три
withLock(mutex1, mutex2):
...
Какого типа должно быть значение val?
это невалидное выражение при данных сигнатурах, валидное должно быть drop(bar(x)), и это гораздо большая проблема если язык вообще позволяет писать такое, раст и не позволяет, но если считать что; это drop, то зачем мне делать drop на каждый вызов того что и так возвращает ()?
Нет никакого let mut, есть let PATTERN
я не очень понимаю как mut может относиться к части pattern
я не очень понимаю как mut может относиться к части pattern
let (a, mut b) = function_returning_pair(arg);
Тут возвращаемое значение деконструируется на a
и b
, но только одно имя позволяет значение менять.
Ок. Признаться за всё время с растом ни разу не делал так. Однако всё равно не готов это сходу отнести маркер мутабельности к паттерн матчингу
Это уже конструирование после сопоставления паттерна.
Этот mut
никак на паттерн не влияет и не является его частью, так же как &
, ref
, ref mut
или биндинг значения к другому имени.
С таким же успехом можно было бы это заменить на
let (a, b) = function_returning_pair(arg);
let mut b = b;
с точки зрения паттерна — это одно и то же.
Просто где-то нужно поставить признак изменяемости данных за биндингом, вместо предполагаемой по-умолчанию константности и это удобно делать сразу при его объявлении. Можно в let, можно в сигнатуре функции.
но если считать что; это drop, то зачем мне делать drop на каждый вызов того что и так возвращает ()?
Можно и не делать. Но ;
помимо этого превращает выражение в инструкцию, то есть выступает разделителем инструкций.
… что можно и не делать (разделять инструкции; ), что доказывают много современных языков. Но вы вроде писали о том как важно помогать парсеру… ценой своего удобства
Я писал о том, что точка с запятой не просто разделяет инструкции, она превращает выражение в инструкцию. У вас строка кода может быть как инструкцией, так и выражением, и если нужно последнее — просто не ставьте точку с запятой.
Осталось понять как то, что вы написали поможет не ставить бесполезный символ в конце каждой строки, никак не поможет
В Rust можно свободно разбивать код одного выражения на несколько строчек. И точка с запятой таки действительно выступает как разделитель а не как ритуальный символ, как в других языках, явным образом завершая выражение (поэтому, собственно, и не-Copy значение окончательно дропается в конце инструкции — оно использовано, но не назначено никакому биндингу и никуда не передано). Точка с запятой меняет смысл выражения и это используется компилятором для проверки корректности программы и ловли случайных ошибок.
Такой подход избавляет от необходимости декларировать тип до того, как будет объявлена переменная этого типа. В C/C++ из-за этого приходится отдельно делать объявления, отдельно определения, и иногда изменение порядка деклараций может изменить семантику.
C# же как-то умеет без деклараций определять переменные и их типы, как в сишном стиле
int a;
так и в похожем как у rust, javascript и тд
var a = 1;
Но и отсутствие ключевого слова — тоже плохо, так как в этом случае возникают ситуации, когда становится невозможно однозначно отличить функциональный тип от других элементов языка
Так в чём проблема была сделать что-то типа такого:
let myFunc: (x: i32) -> bool;
В общем и целом складывается впечатление, что хотели сделать что-то типа околофункционального ЯП, но в итоге сделали просто, на скорую руку и синтаксис в итоге выглядит так, чтобы его просто легче было парсить, хотя другие ЯП с этим вполне себе справляются (начиная от перегрузок операторов, заканчивая контекстным анализом синтаксиса).
Я пробовал кодить на расте и некоторые моменты мне понравились, но пока язык выглядит сыровато по моим ощущениям. Буду следить за развитием этого языка, потому что из него действительно может получиться хороший инструмент для разработки.
Оффтоп: Всех с наступающим!
C# же как-то умеет без деклараций определять переменные и их типы, как ...Что же хорошего в нескольких способах записи одного и того же? В командах наоборот ограничивают такую креативность проверками стиля кода.
хотели сделать что-то типа околофункционального ЯПХотели сделать более последовательный и безопасный типизированный императивный ЯП без устаревшего багажа и сделали.
Если мне нужно создать переменную типа int в шарпе — нет никакого смысла писать
var a = new int();
потому что гораздо читабельнее будет
int a;
Однако если создаётся какой нибудь
ObservableCollection<LongGenericTypeName> collection = new ObservableCollection<LongGenericTypeName>() ;
гораздо удобнее и читабельнее написать
var collection = new ObservableCollection<LongGenericTypeName>() ;
потому что гораздо читабельнее будет int a;
Не читабельнее, чем если было бы
var a: int;
Зато есть единственный синтаксис объявления переменной. Сейчас в С-подобных ЯП по сути просто развитие старого невыводящего типы анализатора, с сохранением старого синтаксиса. Поддержка int a; не есть сознательный «сахар» для удобства разработчиков, это на 100% легаси. Посмотрите как делается новый ЯП:
1) На базе Java developer.android.com/kotlin/learn#variables
2) После C# то же самое www.tutorialspoint.com/fsharp/fsharp_variables.htm
Разработчики других новых ЯП принимают точно такие же решение, как и Rust. Потому что добавлять новый синтаксис, чтобы писать на 4 символа меньше — это добровольный отстрел ноги для будущего ЯП.
let myFunc: (x: i32) -> bool;
Ну в статье же приведен пример, когда это вызывает проблемы:
let myFunc: (i32, i32);
Что это, функция или кортеж?
А вот это функция:
let myFunc: (i32, i32) -> ();
И нет неоднозначности, просто взять приоритет у оператора ->, и уже по контексту понятно, что это аргументы функции, а не кортеж.
Насколько я понимаю, язык помимо прочего стараются делать максимально контекстно-свободным, в том плане что по-возможности каждая конструкция имеет уникальный префикс и мы заране знаем что дальше. То есть не надо откатываться назад и начинать разбор повторно. В вашем случае мы в позиции let myFunc: (i32, i32)
не знаем, есть ли дальше ->
или нет, и в зависимости от этого трактуем или как функцию, или как кортеж. В языке стараются таких ситуаций не допускать, поэтому например по префиксу fn даже дальше парсить не надо: знаем, что там функция.
Контекстная свобода — это независимость интерпретации от контекста. Пример контекста — число открытых скобочек. То же, о чём вы говорите, к контекстам не имеет отношения. Разные конструкции с общим префиксом — типичная ситуация. Например: const
и continue
.
Это одна и та же конструкция: в одном случае это будет Keyword("const")
, а в другом — Keyword("continue")
. Как будто вы никогда компиляторы не писали...
По моему
fn (i32, i32)
Выглядит чище, чем
(i32, i32) -> ()
Во втором случае используется много символов для указания того, что функция ничего не возвращает, и пишем мы их только ради того, чтобы обозначить тип как функцию.
По мне вторая запись информативнее: отображение кортежей из двух целых на единственное значение. Ещё бы встроенную возможность распаковывать кортежи для передачи параметров в функции, например, как в питоне: myfun(*(1, 2))
много символов для указания того, что функция ничего не возвращает
Дык, ваша функция таки возвращает значение — юнит. Из функций, не возвращающих значение, нет возврата вообще (бесконечный цикл или паника, например), и запись fn (i32, i32)
в равной степени может означать и то, что тип возврата — bottom.
отображение кортежей из двух целых на единственное значение
Функции с такой сигнатурой пишутся ради сайд-эффектов, ставить их в один ряд с функциями действительно возвращающими значение особого смысла нет.
Функции, "возвращающие bottom", точнее не возвращающие управление — это такая редкость, что написать -> !
по такому случаю не сложно.
Так в чём проблема была сделать что-то типа такого:
let myFunc: (x: i32) -> bool;
В том что у лямбд в расте уникальный тип, который реализует только общий интерфейс "функция с сигнатурой i32 -> bool".
то есть
let x = 5;
let lambda = if 1 > 2 { || 10 } else { || x };
error[E0308]: `if` and `else` have incompatible types
--> src/lib.rs:3:44
|
3 | let lambda = if 1 > 2 { || 10 } else { || x };
| ----- ^^^^ expected closure, found a different closure
| |
| expected because of this
|
= note: expected type `[closure@src/lib.rs:3:29: 3:34]`
found closure `[closure@src/lib.rs:3:44: 3:48]`
Так что отдельный синтаксис для фунок нужен в любом случае
Я пробовал кодить на расте и некоторые моменты мне понравились, но пока язык выглядит сыровато по моим ощущениям. Буду следить за развитием этого языка, потому что из него действительно может получиться хороший инструмент для разработки.
Полагаю это ложное ощущение. По крайней мере после шарпа и тайпскрипта раст воспринимается на ура.
В том что у лямбд в расте уникальный тип, который реализует только общий интерфейс «функция с сигнатурой i32 -> bool».
И что в этом хорошего?
Компилятор может их лучше соптимизировать, ведь нет индирекции и виртуализации. Для гц языков это может и не сильно важно, а вот для низкоуровневых С/С++/Раста это важно
Тем, что код функции, вызываемой при вызове замыкания, известен на этапе компиляции и его адрес можно подставить по месту, а потому сам экземпляр замыкания занимает ровно столько памяти, сколько занимают захваченные значения (с поправкой на выравнивания). В частности, если замыкание не захватывает вообще ничего, то оно вообще не занимает места в памяти:
let adder = |x: u32| x + 10;
println!("{}", std::mem::size_of_val(&adder)); // печатает `0`
Лямбда-функции в C++, если не ошибаюсь, работают похожим образом.
Причина не в этом. Лямбды захватывают контекст, который тоже является частью типа. Если контекст не захватывать, то тип одинаковый и все компилируется.
Ваш пример компилируется в силу того, что обе лямбды неявно приводятся к функциональным указателям, что возможно в силу того, что они не захватывают контекст:
let func = if 1 > 2 { || 10 } else { || 5 };
// тот же размер, что и у указателя
assert_eq!(
std::mem::size_of_val(&func),
std::mem::size_of::<usize>(),
);
Не знаю, спорить не буду, но что-то я сомневаюсь, что типы так приводятся. Все же в Rust довольно строгая система типов, автоприведения в большинстве случаев нет.
А чего сомневаться-то, просто укажем заведомо неверный тип:
let () = if 1 > 2 { || 10 } else { || 5 };
И компилятор всё сразу скажет:
error[E0308]: mismatched types
--> src/main.rs:2:5
|
2 | let () = if 1 > 2 { || 10 } else { || 5 };
| ^^ expected fn pointer, found `()`
|
= note: expected fn pointer `fn() -> {integer}`
found unit type `()`
Заметьте, никаких [closure@main.rs:2:12]
В данном случае оно есть, и можно видеть что код компилируется. Потому что индивидуальные типы компилируются только для замыканий, которые захватывают переменные, а все функции без замкнутого окружения в пределах одной сигнатуры разделяют один тип.
let x : fn() -> i32 = || 10;
let y : fn() -> i32 = || 20;
Как видно, в отличие от замыканий у них один-единственный (и называемый) тип.
все функции без замкнутого окружения в пределах одной сигнатуры разделяют один тип
… Если их тем или иным способом привести к функциональному указателю, что ты и сделал. Так-то это всё-таки разные типы:
trait TypeEq<T> {}
impl<T> TypeEq<T> for T {}
fn take_equal_types<T: TypeEq<U>, U>(_: T, _: U) {}
fn main() {
take_equal_types(|| 10usize, || 10usize); // error[E0308]: mismatched types
}
Вы слышали когда-нибудь про риторические вопросы?
let message = "Hello!".to_string();
message;
println!("{}", message); // error[E0382]: borrow of moved value: `message`
я сам написал бы про такой язык что-нибудь токсичное…
Какую проблему вы видите в этом коде?
Ответ довольно таки однозначен, но также очевидно вы либо не знакомы с с/с++ либо не считаете это проблемой. В с/с++ (а может и в других языках тоже) строка message; гарантированно НИЧЕГО не делает и будет удалена компилятором тогда как в расте это скрытый дроп. Вот разница в ожидаемом поведение и реальностью и есть проблема. Так уж случилось что с/с++ появились существенно раньше раста и сформировали семантику ожидаемого поведения для тех кто знакомиться с растом и имеет сишный опыт. Появись раст раньше си тогда бы сишное поведение вызывало заслуженный вопрос — чё за нах.
ошибка потому что владение ушло к кому-то другому, но к кому?
*картинка с пилотом*
let _ = message;
Владение ресурсом переходит к переменной `_`, и далее обращение к `message` недопустимо (с оговоркой что `message` не реализует Copy).
Это не переменная: попробуйте скомпилировать
fn main() {
println!("{}", _);
println!("{}", _s);
}
Сразу видно, что _s
— переменная, а _
— нет.
error: expected expression, found reserved identifier `_`
--> src/main.rs:2:20
|
2 | println!("{}", _);
| ^ expected expression
error[E0425]: cannot find value `_s` in this scope
--> src/main.rs:3:20
|
3 | println!("{}", _s);
| ^^ not found in this scope
Ещё где‐то на Rust’овских форумах где‐то было обсуждение по поводу того, что let _ = foo
приводит к немедленному освобождению foo
, а сохранение в неиспользуемую переменную — к освобождению по выходу из контекста, из‐за чего что‐то работало неправильно.
Если это плохо- то скажите как получить подобное поведение в С++? В плюсах невозможно отметить переменную убитой на произвольной строке (фигурные скобки не обеспечивают всех инвариантов), так что вот типичный use after free: delete(message); print(messsage); с которым в общем виде не возможно бороться в C++
Независимо от того, делает ли строка message;
хоть что-то или является скрытым дропом — ей не место в нормальном коде.
message;
— просто частный пример для иллюстрации того, что происходит. В реальном коде будет foo();
с дропом в этом месте значения, которое вернула foo
, который может приводить к побочным эффектом, в случае кастомизации Drop
'а этого значения.
Ну это всего лишь ваше мнение, я прекрасно могу представить что такой код может появиться в раст для оптимизации, например закрыть файл раньше чем завершится скоуп где обьявленна файловая переменная. drop(file); банально сложнее набирать. У меня лично персональная ненависть к скобкам и я бы точно предпочел file;
Я точно знаю, что вы отлично разбираетесь в с++, но в вашем примере я аналог message; не нашел вместо этого вы иллюстрируете побочный эффект мува что совершенно точно не тоже самое. Я думаю вы просто уже подзабыли из за долгово неиспользования с++ что единственная разумная причина такого кода на с++ это подавление ворнинга о неиспользуемой переменной. Есть 100500 макросов которые генерируют именно такой код — переменная; #define unused(x) x; Смысл в том что семантика раста в этом случае это дестрактив мув которого нет в с++, но это и есть проблема т.к. в с это просто ничего.
есть такой атрибут [[nodiscard]]. И вот какой нибудь очень талантливый человек решил, что он точно знает, что вот ни в коем случае нельзя игнорировать возвращаемое значение, но вы с ним почему то не согласны. Как предлагаете решать такое противоречие в проекте где ворнинги это ошибка? Что насчёт условной компиляции, где для одной платформы переменная используется, а для другой — нет?
#pragma GCC diagnostic ignored "-Wwhatever"
Так и пример с точкой запятой немного надуманный, на практике будет drop(resourse);
. Чуть длиннее, зато не надо ломать голову над тем, почему так написано.
Тогда как на расте это совсем другой код и смысл претензии в том что, раст заимствует синтаксис из с да ещё и по возможности с той же семантикой, что бы больше людей быстрее научилось понимать раст, а тут такое дело что; и конкретно очень распространённый хак на с имеет совсем другую семантику в расте ставя под сомнение обоснованность заимствования; т.к. это не помогает понять, а наоборот мешает.
Да, я вижу люди утверждают, что такой код на расте плохой/не идиоматичный, другие добавляют, что он и на с/с++ тоже смысла не имеет. Вот с этим спорю уже я — на с/с++ он имеет смысл и встречается довольно часто, т.к. альтернатива синтаксически ещё хуже. На расте я лично тоже вижу в нем смысл и предпочел бы его явному дропу, потому что так короче — это моё персональное предпочтение чем короче тем почти всегда лучше.
Если уж у меня была возможность это поменять я бы смотрел в сторону оператора — destructive move. Ну например как то так
= message
читается как переместить содержимое message в пустую переменную == дропнуть месседж. С таким синтаксисом было бы сложнее сделать ошибку вида
// что то возвращает и дропается
foo(message)
Вместо того что бы втихую игнорить возвращаемое значение компилятор бы ругался, требуя либо объявить переменную
x = foo(message)
либо явно дропнуть ее
= foo(message)
Да, я признаю что это тоже не идеальное решение, т.к. другим людям может быть не важно игнорируется возвращаемое значение или нет, а вот лишний знак равно придётся писать. Но я там на верхнем уровне оставил комментарий, что есть другой путь где и писать = не нужно, но при этом при чтении = будет явно присутствовать. Смотри habr.com/ru/post/532660/?reply_to=22488134#comment_22488090
Думаю раст не ругается на это для случаев вроде такого:
print_something(x); // например возвращает число записанных байт
с вашими правилами получается, что компилятор будет требовать
_ = print_something(x);
что конечно тоже возможно, но наверное не очень полезно для нас как программистов.
Вот это "не важно игнорируется возвращаемое значение или нет" на самом деле довольно часто именно так и есть, для случаев когда возвращаемое значение игнорировать не стоит есть атрибут, который на подобное использование выдаст варнинг "меня забыли использовать". Но в подавляющем числе случаев если такой пометки нет то "вернулось и дропнулось, а нам пофиг" — самый частый сценарий такой операции.
набираю в IDE: foo(message)
IDE авто заменяет на: = foo(message)
я такой, о точно забыл обработать значение, и дописываю
x = foo(message)
или да все правильно мне пох в этом случае на возврат значения
if (foo)
bar()
Но следом вы рассказываете про точку с запятой и приводите следующий пример
if x < 42 {
foo(x);
} else {
bar(x)
}
Помоему это ужасно.
Я представил сиутацию когда мне нужно найти ошибку в чужом коде и я встречаю подобную конструкциию. Как мне понять, что хотел сделать автор? Намеряна ли пропущена; после bar() и намерянно ли она есть поcле foo()? Как мне понять без реверсинга всей относящейся логики что тут нет опечатки?
В общем двойственное ощущение, с одной стороны хотят чтобы кодеры по меньше делали ошибок, а с другой изначально закладывают синтаксис, который способствуют ошибкам.
К счастью, поведение кода от наличия точки с запятой после bar(x)
не меняется (при условии, что оба варианта компилируются). Так что подобный странный код можно просто не писать...
Если данный код скомпилировался (или прошел check
), то вы точно знаете, что bar
возвращает ()
, так как другая ветка условия возвращает именно это значение благодаря ;
, а типы значений веток обязательно должны совпадать.
Этот пример просто для иллюстрации данного факта, так лучше не писать. Хотя в реальном продакшн-коде я часто замечаю пропущенную точку с запятой в похожих ситуациях, когда поставить забыли или не захотели, но еще ни разу это не привело ни к какой проблеме понимания кода или к какой-то иной ошибке.
Просто bar(x)
возвращает () и bar(x);
возвращает его же. Можно писать а можно нет, это не ошибка любом случае: семантика обеих записей совпадает. Я для симметрии всегда пишу
А многие функции и замыкания избавляются от лишнего синтаксиса с оператором возврата:
fn add_two(a: isize) -> isize {
a + 2
}
вместо
fn add_two(a: isize) -> isize {
return a + 2;
}
а есть примеры где это даёт более явный выигрыш? Экономия одного слова это мелочь и не прям супер выигрыш и возможно это вкусовщина. Скажем я бы предпочел увидеть явный оператор возврата, чтобы не угадывать по флоу какое выражение было вычислено последним, подозреваю что это «последнее вычисленное выражение» (по опыту с groovy) может являться источником неясности и потенциалом для выстрела в ногу. В общем эта экономия выглядит как-то сомнительно. Также выходит что в одних местах оправдывается многословие как необходимое, а в других оправдывается срезание синтаксиса «для выразительности»
скажем Python достаточно выразительный язык, но там не стали экономить на return
Да вообще не понимаю, чего все ополчились на Rust. Нормальный язык с нормальным синтаксисом, надо просто мозгом думать.
это цитата не автора статьи, но как же смешны подобные рассуждения про «надо просто мозгом думать». Вот если бы все так и рассуждали, про язык, потом про библиотеки, потом про архитектуры с этой мантрой «надо просто мозгом думать» («я тут наархитектурил, а если ты не понял то ты тупой или ленивый») то немного бы мы так написали полезного софта. Кроме языка и так много о чем надо думать, язык это самый нижний слой, это просто инструмент. Представьте себе если бы человеческие автоматизмы не работали и надо было бы осознанно думать как ходить, как переставлять одну ногу, потом другую, потом как дышать, как делать вдох, потом как выдох и тд. Надеюсь аналогия понятна.
а есть примеры где это даёт более явный выигрыш?
Любые лямбды. В выражении |x| { return x + 1; }
слово return визуально занимает половину пространства (пусть по символам и выходит "всего-то" 6 из 20). А такие выражения приходится писать довольно часто.
чтобы не угадывать по флоу какое выражение было вычислено последним
Можете уточнить, в каких ситуациях это составляет проблему? Просто посмотреть на последнюю строчку в теле функции.
Также выходит что в одних местах оправдывается многословие как необходимое, а в других оправдывается срезание синтаксиса «для выразительности»
return
оправдывается как многословный потому, что он может встретиться в любой строчке внутри функции, вам его придется искать и вы должны хорошо его видеть, ибо он меняет поток выполнения программы. А вот в последней строчке он не нужен, так как явно или неявно присутствует там всегда (в Rust нет процедур) и поток выполнения там он не меняет.
Что же касается просто блоков, то там вообще оказывается не применим return
, потому что это именно оператор возврата из функции, а не из текущего блока. Поэтому в блоках можно возвращать только последнее выражение.
А вот в последней строчке он не нужен, так как явно или неявно присутствует там всегда (в Rust нет процедур) и поток выполнения там он не меняет.
аа, теперь я понял. А если в конце функции в последнем выражении поставить точку с запятой то это уничтожит значение? Я сужу по прошлому обсуждению и выглядит так что точка с запятой вызывает drop
откомменчу тут же и это
Любые лямбды. В выражении |x| { return x + 1; } слово return визуально занимает половину пространства (пусть по символам и выходит «всего-то» 6 из 20). А такие выражения приходится писать довольно часто.
эта экономия имеет смысл для коротких и однострочных лямбд. Но в других языках, хотя бы некоторых (даже вербозной джаве) однострочные лямбды не требуют return а многострочные требуют. По-моему вполне нормально.
аа, теперь я понял. А если в конце функции в последнем выражении поставить точку с запятой то это уничтожит значение?
Да, но у вас так код почти наверняка не скомпилируется.
Да, уничтожит, но в подавляющем большинстве случаев именно это и требуется.
Вопрос единообразия. Ретурн не нужен в лямбдах, однострочных функциях, ифах и так далее. Это просто вопрос консистентности.
А если в конце функции в последнем выражении поставить точку с запятой то это уничтожит значение?
Уничтожит, но вы получите ошибку компиляции, если типы возврата не совпадут:
fn add_two(a: isize) -> isize {
a + 2; // error[E0308]: mismatched types, expected `isize`, found `()`
}
а есть примеры где это даёт более явный выигрыш?
Вот явных примеров я не покажу, но по опыту могу сказать, что код на Rust из-за этого читать проще, потому что явный return
— это теперь всегда именно ранний возврат.
скажем Python достаточно выразительный язык, но там не стали экономить на return
Получи возможность забыть написать return
и получить None
в качестве возвращаемого значения вместо того, что было вычислено. И да, мне кажется, называть выразительным язык, в котором лямбды в принципе не могу больше одной строчки занимать — это немного смешно.
Зато в очень выразительном, например, JS, где лямбда в лямбде и лямбдой погоняет, очень интересно разбирать по десять подряд анонимных функций в стеке исключения.
Получили возможность забыть написать return и получить None в качестве возвращаемого значения вместо того, что было вычислено.
Да, это проблема в динамически типизируемом языке, но в расте такая ошибка всплыла бы на этапе компиляции.
Отсутствие явного return
для выхода из функции действительно можно считать за плохой тон, поначалу. Например, задумавшись, как грепать места возврата из функции… Я для себя решил, что всегда буду писать return
, ибо нефиг...
Но оказалось, что его опускание как-то само-собой получается, дискомфорта не испытываю. Можно сказать, что при отсутствии явного ключевого слова также стараешься делать функции поменьше, чтобы с первого взгляда было видно, где у нас возврат. А фактически, возврат из функции где-то в середине ее тела без явного ключевого слова возможен только при использовании больших if/match
, а иначе в середине естественным образом появляются эти return
.
И я говорю не только за свой код — читая код стандартной библиотеки и других проектов (serde) тоже замечаю, что даже без наличия return
читаемость не страдает
А если скажем есть блок if… else if… else и в каждом из этих ветвлений есть по 4-5 команд, то мне надо искать глазами последнюю в каждом из блоков чтобы понять что пойдёт в ретурн?
так а блоки из функции сами по себе никогда не возвращаются. Возвращает только последняя строчка. Если же в последней написано:
fn foo() {
...
if some_cond { 10 }
else {
if some_other_cond { 20 }
else { 50 }
}
}
То вроде и не надо напрягаться, чтобы понять, что пойдет в ретурн.
Даже если там по 2 экрана кода, смотреть надо на последнюю строчку из этих экранов. А вообще — рефакторинг, экстракт метод и вот это все спасут отца русской демократии. На практике (а не при теоретизированнии в комментах) могу сказать, что опускание ретурна очень удобно. Регулярно этим пользуюсь.
Вот я и говорю, что как-то само собой получается, что 2 экрана не выходит. Естественно, если будет получатся 2 экрана, то я задумаюсь о выделении в отдельную функцию (почти наверняка) или добавлю ключевое слово (хотя наверняка это будет из-за желания уменьшить количество отступов в первую очередь). Но вот говорю как есть — пока такого в моей практике не встречалось.
Как человек, который пишет на Rust по работе, могу сказать, что это проблема надумана.
Естественно, если будет получатся 2 экрана, то я задумаюсь о выделении в отдельную функцию
тогда получается что опускание ретурнов дополнительно отягощает другие проблемы.
Сейчас мне приходится иметь дело с большими объёмами не очень красивого кода, в том числе с длинными методами и блоками. В моём случае опускание return усугубило бы ситуацию. И исправить положение рефакторингом я не могу т.к. «нельзя просто взять и отрефакторить», как вы сказали, десятки тысяч строк кода. Мне даже никто не даст это сделать, т.к. это время и деньги. Т.е. мне приходится читать то что другие написали «как им было удобно писать» (улавливаете иронию ситуации?)
я пока так из этой ветки не понял как понять что метод что-то НЕ возвращает, мне смотреть на сигнатуру или искать глазами точку с запятой?
Метод не может ничего не возвращать, просто иногда он возвращает юнит-тип — тип с единственным значением. По некоторому недоразумению кастрированный юнит в си-лайк языках носит название void
. Кастрированный в том плане, что ни в переменную его не сохранишь, ни аргументом явно не передашь (хотя как раз в ANSI C вроде можно, но в последующих языках уже нет)
тогда получается что опускание ретурнов дополнительно отягощает другие проблемы.
Сейчас мне приходится иметь дело с большими объёмами не очень красивого кода, в том числе с длинными методами и блоками. В моём случае опускание return усугубило бы ситуацию. И исправить положение рефакторингом я не могу т.к. «нельзя просто взять и отрефакторить», как вы сказали, десятки тысяч строк кода. Мне даже никто не даст это сделать, т.к. это время и деньги.
По-моему опыту функции в 2 экрана возникают не из-за ретурнов, и ретурны никак с ними не помогут.
По-моему опыту функции в 2 экрана возникают не из-за ретурнов, и ретурны никак с ними не помогут.
конечно это верно, но я говорю о том что опускание return добрасывает еще одну какашку сверху этого беспорядка. И это часто нельзя решить рефакторингом, т.к. нельзя же рефакторить абсолютно всё что читаешь (именно читаешь, а не пишешь), это могут быть десятки тысяч строк кода. Есть разница между «так удобно писать» и «так удобно читать». Профессионалы тем и отличаются, что пишут код не на любителя, а такой в котором смогут разобраться и читать без боли все или хотя бы большинство.
По некоторому недоразумению кастрированный юнит в си-лайк языках носит название void. Кастрированный в том плане, что ни в переменную его не сохранишь, ни аргументом явно не передашь
а для чего это может понадобиться?
конечно это верно, но я говорю о том что опускание return добрасывает еще одну какашку сверху этого беспорядка.
Кто сказал что это какашка? У меня вот мнение, что это наоборот шоколадка. Кто прав?
И это часто нельзя решить рефакторингом, т.к. нельзя же рефакторить абсолютно всё что читаешь (именно читаешь, а не пишешь), это могут быть десятки тысяч строк кода.
Что во что рефакторить, отсутствие ретурна в его присутствие? Добавление необязательных элементов (вроде скобок там, где их можно опустить) я за рефакторинг бы не стал считать.
По-моему опыту (тоже из практики) все топят за специфичные причудливые особенности ровно до тех пор, пока не сталкиваются с причудливыми особенностями которые использует кто-то другой
Причудливые особенности которые используются в каждом первом ML языке? Ну простите, это скорее не особенности, а синдром утенка у некоторых людей. Которые когда говорят "я знаю много языков" оказывается имеют в виду "Си, Сишарп, Сиплюсплюс, джава, джаваскрипт". В итоге думают, что у них кругозор огого, а на самом деле они просто с разных сторон неплохо знают сишку.
На прошлой неделе мне пришлось разбираться в целой подсистеме, которая написана на… Scala. «О привет, путник пожаловавший в наш сервис.
На прошлой неделе мне пришлось разбираться в целой подсистеме, которая написана на… %langname%. «О привет, путник пожаловавший в наш сервис.
потом, делать подавление возврата с помощью точки с запятой… серьёзно? Это же 100% caveat для всех кто пришел с языков где точка с запятой это просто конец оператора.
Оно работает точно так же как и в любом другом языке. Точка запятой — разделитель, возвращаемое значение — юнит тайп, в сишке это void
. В чем различие с другими сишными языками-то?
Кто сказал что это какашка? У меня вот мнение, что это наоборот шоколадка. Кто прав?
eсли есть крупные блоки ветвлений, неудобно отслеживать возврат в 3-5 пунктах, 1 точку проще. (мне проще, ну может я странный, да)
Что во что рефакторить, отсутствие ретурна в его присутствие? Добавление необязательных элементов (вроде скобок там, где их можно опустить) я за рефакторинг бы не стал считать.
я говорю о том что невозможно рефакторить весь код который плохо читается. Даже если это минорные изменения, которые вы не считаете за рефакторинг, чтобы поменять это, придётся делать много пулл реквестов и проходить ревью (в крупных проектах это долго)
В итоге думают, что у них кругозор огого, а на самом деле они просто с разных сторон неплохо знают сишку.
если вы про предвзятость, то она есть у всех. Этот аргумент работает в обе стороны, старшие часто более консерватины, а молодые слишком падки на всё новое. Если Rust не может убедить программистов с багажом, тогда придётся ему ждать поколение без сишки. Надо только продержаться до этого времени. Убедить программистов с багажом помогло бы отсутствие причудливых специфичных решений. Даже эта статья называется «Так ли токсичен синтаксис Rust?». На воре и шапка горит
Если Rust не может убедить программистов с багажом, тогда придётся ему ждать поколение без сишки. Надо только продержаться до этого времени. Убедить программистов с багажом помогло бы отсутствие причудливых специфичных решений.
Ниже PsyHaSTe процитировал заметку Эрика Липперта, что это очень сложный trade-off: если в языке слишком много причудливых специфичных решений, то на него слишком сложно переходить; если слишком много легаси — то на него нет смысла переходить.
По-моему, в C# очень удачный баланс между легаси и новизной, даже если бы могло быть и лучше. F# и прочие попытки выбросить из языка легаси — переманили в свои ряды совсем небольшую долю программистов. С другого конца спектра можно вспомнить мертворождённую J#.
eсли есть крупные блоки ветвлений, неудобно отслеживать возврат в 3-5 пунктах, 1 точку проще. (мне проще, ну может я странный, да)
Покажите пример, а то без примера непонятно. Вот сколько на расте пишу, не помню чтобы с этим были какие-то проблемы.
я говорю о том что невозможно рефакторить весь код который плохо читается. Даже если это минорные изменения, которые вы не считаете за рефакторинг, чтобы поменять это, придётся делать много пулл реквестов и проходить ревью (в крупных проектах это долго)
Так как раз код с ретурнами читается плохо, а без них — лучше.
если вы про предвзятость, то она есть у всех. Этот аргумент работает в обе стороны, старшие часто более консерватины, а молодые слишком падки на всё новое. Если Rust не может убедить программистов с багажом, тогда придётся ему ждать поколение без сишки. Надо только продержаться до этого времени. Убедить программистов с багажом помогло бы отсутствие причудливых специфичных решений. Даже эта статья называется «Так ли токсичен синтаксис Rust?». На воре и шапка горит
Просто некоторым нужна более быстрая лошадь, а автомобиль они ругают за то что у них посадка другая и седло не накинешь. Что поделать, некоторым непонтяно, что сделать лошадь которая будет бежать 200км/ч нереально, и "что-то нужно менять" чтобы это стало возможным.
Ниже PsyHaSTe процитировал заметку Эрика Липперта, что это очень сложный trade-off: если в языке слишком много причудливых специфичных решений, то на него слишком сложно переходить; если слишком много легаси — то на него нет смысла переходить.
Абсолютно верно
Покажите пример, а то без примера непонятно. Вот сколько на расте пишу, не помню чтобы с этим были какие-то проблемы.
чтобы показать пример, мне нужно придумывать полотна кода. Если я вставлю просто троеточия то вы скажете «ну и в чем проблема, я прекрасно вижу точки возврата?». В реале глаз будет более замылен и блоки тоже могут быть больше.
ну пусть.
Metric calculateEngamementMetric() {
...
...
...
...
...
if () {
...
...
...
...
...
...
...
...
...
result1 = doSmth()
trackUserActivity();
result1
} else if () {
...
...
...
...
...
doSmth2().match {
- return err
}
} else {
...
...
...
...
...
result3 // тут я уже не помню что у меня возвращает функци, мне надо смотреть на точку с запятой - не подавляется ли результат
}
}
этот код можно улучшить если завести переменную result и указать её в конце функции. Только мне надо смотреть на то нет стоит ли там точка с запятой чтоб понять возвращает что-то функция или нет — после этого полотна кода я могу уже не помнить сигнатуру функции. Раньше я бы смотрел на return, теперь надо смотреть на менее заметную точку с запятой
Просто некоторым нужна более быстрая лошадь, а автомобиль они ругают за то что у них посадка другая и седло не накинешь.
ну одно дело когда это стал автомобиль, а другое если у лошади убрали седло и натянули его на копыта, а копыта вставили в хвост
если раньше мне приходилось смотреть на return то теперь надо смотреть не подавляется ли возврат. В чем тут инновация?
Так как раз код с ретурнами читается плохо, а без них — лучше.
мы кажется ходим по кругу. Мы выше уже осуждали это. Я назвал почему это плохо (для меня), вы сказали потом что можно легко отрефакторить. Дальше я сказал что это не так легко, и вы вернулись к началу — что и так хорошо.
я в целом озвучил что хотел, я не смогу убедить вас, а вы меня. С наступившим! :)
Один вопрос, а нахрена писать такой код, как вы показали?
во-первых ретурны этому коду не помогут никак. Он просто плох как есть
во-вторых "подавить точкой с запятой" у вас ничего не выйдет, потому что компилятор пожалуется что не все ветви кода возвразщают значение.
этот код можно улучшить если завести переменную result и указать её в конце функции.
Этот код можно улучшить с помощью рефакторинга "экстракт метода" а также опционально "замена условного ветвления полиморфизмом" (всё по Фаулеру, ага). И тогда будет:
Metric calculateEngamementMetric() {
...
...
...
...
...
if () {
getResult1()
} else if () {
getResult2()
} else {
getResult3()
}
}
Или даже с ретурнами (раст их не запрещает если что, я все время ими пользуюсь для того чтобы happy path был линеен):
Metric calculateEngamementMetric() {
...
...
...
...
...
if () {
return getResult1();
}
if () {
return getResult2();
}
getResult3()
}
ну одно дело когда это стал автомобиль, а другое если у лошади убрали седло и натянули его на копыта, а копыта вставили в хвост
если раньше мне приходилось смотреть на return то теперь надо смотреть не подавляется ли возврат. В чем тут инновация?
Не надо никуда смотреть, подавить что-то случайно чтобы оно скомпилировалось не выйдет. Это просто короткая запись для drop(expr)
, вместо этого можно писать просто expr;
. Дропнуть что не нужно у вас просто не выйдет.
Не надо никуда смотреть, подавить что-то случайно чтобы оно скомпилировалось не выйдет. Это просто короткая запись для drop(expr), вместо этого можно писать просто expr;. Дропнуть что не нужно у вас просто не выйдет.
я говорю о ЧТЕНИИ кода
про рефакторинг это уже 3-й круг, я уже писал об этом.
Один вопрос, а нахрена писать такой код, как вы показали?
я как бы не призывал такой код писать. Но вот читать мне подобный код нередко приходится. Когда (и если) Rust станет мейнстримом, выпейте за меня бокальчик когда увидите такой же плохой код на Rust ))
По-моему, в C# очень удачный баланс между легаси и новизной, даже если бы могло быть и лучше. F# и прочие попытки выбросить из языка легаси — переманили в свои ряды совсем небольшую долю программистов. С другого конца спектра можно вспомнить мертворождённую J#.
вот-вот, я примерно это и имею в виду
я говорю о ЧТЕНИИ кода
про рефакторинг это уже 3-й круг, я уже писал об этом.
Ну вот нет никаких проблем при чтении
я как бы не призывал такой код писать. Но вот читать мне подобный код нередко приходится. Когда (и если) Rust станет мейнстримом, выпейте за меня бокальчик когда увидите такой же плохой код на Rust ))
Этот код плох не из-за ретурнов, и что есть ретурны что нет разницы никакой не будет.
вот-вот, я примерно это и имею в виду
сишарп был хорош до +- 5 версии, а стех пор он только сахаром обрастает, а полезных фичей раз-два и обчелся за все последние 5 лет :(
сишарп был хорош до +- 5 версии, а стех пор он только сахаром обрастает, а полезных фичей раз-два и обчелся за все последние 5 лет :(
меня язык вполне устроил бы, вопрос больше в экосистеме — мне кажется с# заточен на MS экосистему. Net core вроде как постепенно меняет ситуацию, но скажем навскидку Google App Engine саппортит c# хуже чем java. Если в докер запаковать то наверняка получится работать в Kubernetes и тд. Что там ещё с другими тулами, скажем драйверами субд и тд, вот это всё — я глубоко не вникал, если кто-то скажет что сейчас там всё круто то я только за.
а как язык с# меня устроил бы и 3 года назад
как понять что метод что-то НЕ возвращает
Смотреть на сигнатуру. В любом случае лучше всегда на нее смотреть, зачастую этого уже достаточно.
я пока так из этой ветки не понял как понять что метод что-то НЕ возвращает, мне смотреть на сигнатуру или искать глазами точку с запятой?
На сигнатуру, конечно, типы не врут.
код на Rust оч экономит ресурсы мозга, тк проще парсить.
ошибкой в первом примере могло быть не отсутствие фигурных скобок, а наличие случайного отступа.
Нет, отступ случайным быть не может, ибо человек ориентируется именно на него. Бардак с отступами может быть лишь у человека, привыкшего к тому, что отступы можно ставить абы как.
В конструкциях if, else, for, while и так далее, блок требуется обязательно!
И после этого вы ещё что-то говорите про лаконичность. Писать 3 строки вместо 1 — это верх расточительства. С отступами было бы 2 — разумный компромис между лаконичностью и дебагом/трейсами.
отчасти это компенсируется тем, что теперь круглые скобки вокруг условного выражения становятся избыточными и их можно убрать
А чего это одни скобочки можно убирать, а другие нет? Почему бы все скобки не сделать опциональными, требуя их лишь для сложных выражений?
Если все тело замыкания состоит только из одного выражения, то фигурные скобки также можно не ставить.
О, а тут почему они опциональны вдруг, а скобки вокруг аргументов — нет?
в Rust блоки очень важны, они являются самостоятельными программными объектами с особыми свойствами.
Во многих языках они "очень важны" и используются для задания вложенных скоупов для переменных. Даже в сях.
Здесь возникает необходимость вставлять nil в конце блока, чтобы set_x возвращал пустое значение, а не результат выражения val = val, то есть не значение val.
Думаю лучше явно игнорировать возвращаемое значение, чем делать это неявно, случайно написав точку с запятой как и везде.
Для динамически типизированных языков подобный код приемлем, потому что они допускают возврат значений разных типов в зависимости от условия, которое проверяется во время выполнения программы.
Discriminated Union есть и в стат типизированных языках. И они позволяют возвращать значения разных типов, а не только одного пустого.
Так что инструкция a; становится аналогом drop(a). Это нужно помнить, чтобы не удивляться
А может не стоило оставлять WTF в языке? Достаточно было бы предоставить функцию drop, которая бы принимала любое значение и ничего не возвращала. А на бессмысленном коде в духе a;
лучше кидать ошибку.
Хорошо, но почему бы не выбрать сокращение подлиннее, такое как fun или func? Это довольно спорный вопрос даже в Rust-сообществе, но fn выглядит более нейтральным, так как не является другим самостоятельным словом и не имеет никаких посторонних значений по звучанию
Разработчики Раста настолько суровые, что не хотят, чтобы программирование на их языке вызывало коннотации с весельем?
такие слова как fn, mut, mod, pub, impl, dyn и ref, после которых идут пользовательские имена, не затрудняют, а улучшают чтение программы, если при этом программист выбирает длинные осмысленные имена
Язык, полный сокращений, приучает программистов тоже сокращать. Теория разбитых окон, все дела.
Преимущества подобного синтаксиса достаточно очевидны, остается загадкой, почему он не устраивает многих критиков языка
Вообще не очень очевидно, почему функции и замыкания имеют совсем разный синтаксис. Особенно глядя на D, где замыкания выглядят как и объявления функций, только без имени:
( a, b ){ ... }
Если использовать для статического и динамического обращения один и тот же разделитель, то становится невозможно различать подобные вызовы
Опять же, D замечательно всё различает, хоть и использует везде точки.
К сожалению оказалось, что от него совсем не просто избавиться: конструкцияA<B>()
выглядела бы логичнее и лаконичнее, но в контексте выражения парсер не может отличить ее начало от операции сравненияA < B
.
Так может не надо было лепить угловые скобки? Опять же, в D используются вообще круглые скобки.
Result!(F, F.Err) parse(FromStr F)(&self) {
// ...
}
не согласен. Смотрите, питон, который считается образцом readability, тоже использует def вместо define. Почему? Потому что короткое служебное слово лучше длинного.
А вот замену and
на &&
и or
на ||
, да ещё и с омонимией объявления closures, я считаю большой эстетической ошибкой в Rust.
питон, который считается образцом readability
Когда-то и земля плоской считалась. С читаемостью у него всё довольно плохо на самом деле.
У Питона очень высокая читаемость на фоне других языков программирования. Вы можете не понимать всех бездн манкипатчинга и интроспекции собственного стека для полифорфизма, но в целом, открыв исходник на python человек с плохим пониманием понимает, что там происходит куда лучше, чем у большинства языков.
Понятно, что в питоне есть свой хитрый мунскпик. Какой-нибудь там [x(**y) for x, y in foo(*bar)]
вызовет боль даже у опытного питониста, но обычные выражения — одни из самых читаемых, которые я видел в индустрии.
У Питона очень высокая читаемость на фоне других языков программирования. Вы можете не понимать всех бездн манкипатчинга и интроспекции собственного стека для полифорфизма, но в целом, открыв исходник на python человек с плохим пониманием понимает, что там происходит куда лучше, чем у большинства языков.
А я не понимаю. Потому что совершенно непонятно, что за сущности принимаются и возвращаются, и для того, чтобы это понять, приходится изучать контекст.
def run(input: Input) -> bool
) начинают проставлять для более простого вхождения в код разработчиков со стороны. То есть типы используются исключительно, чтобы улучшить читаемость, ни о какой корректности пока не ведётся речь (но когда везде проставлены, то нет смысла не включать анализатор).О, то есть мы сначала объявляем типы ненужными и проверяем их в рантайме, а потом таки ставим хинты, которые ни на что не влияют и которые нужно проверять сторонним инструментом! Фантастическая рациональность.
Хинты появились в питоне в 2010-х, хотя мышление основной массы разработчиков пока инертно. Но, тем не менее, вышеупомянутая мотивация имеет место.
Речь о том, что сначала объявили типы "устаревшими", которые "ограничивают программиста", а потом в течение пары десятков лет наткыкались на детские "внезапно вместо числа пришла строка", которые сначала лечатся какими-нибудь дикими правилами приведения, а потом выработали практики как этого не допустить, и в итоге приходим к тому, что типы-то оказывается не такие уж бесполезные были.
Потом идут последствия рефакторинга — IDE практически беспомощны без тайпхинтов, надежда после любого изменения контрактов только на полноту юнит-тестов, которая не может быть формально гарантирована.
Далее следуют ошибки, когда разработчик опирается на интерфейс структуры без каких-либо оснований, например, ошибки динамического/ad-hoc диспатча, недоконвертация внешних данных во внутренние структуры, и т.п.
Про то, что можно словить проблемы при type juggling я знаю только теоретически. Да, практики порешали, но эти практики (в основном hexagonal arch & clean code) имеют смысл и безотносительно типов.
По моему опыту проблемы «внезапно вместо числа пришла строка» в реальной практике не существует.
Да-да, именно поэтому если к какой-либо ЖС либе нет документации то воспользоваться ей тупо невозможно. Сидишь по сорцам пытаешься понять, какую форму должен иметь объект. И оказывается, что если вместо 12 передавать "12" то всё чудесным образом начинает работать.
типы в IDE покажут что это за объект, дадут автокомплит по методам) в PHP тоже нету проверки во время компиляции. Это интерпретируемые языки. исключение составляет тайпскрипт, который является историческим костылём над тем, что уже все используют и поэтому трудно это поменять
перечисления работают за счетмагииметаклассов, а не являются частью синтаксиса, поэтому никаких Result/Option как в Rust
Нет, не поэтому.
Option[T] = Union[None, T]
. Для Result это будет Union[T, Err[E]]
, можно иначе, но в любом случае как-то так.Если не хотите думать, что правильно, то можно использовать github.com/dry-python/returns
во-первых && и || привычнее родному сишному глазу
во-вторых все остальные операторы std::ops именно что операторы. Делать исключение для булеанов, да при том что & и | как бинарные операторы на тех же булках никуда не денешь, хотя бы ради сокращенной записи x &= bar; y |= baz, наверное было бы плохой идеей. Тем более что or/and различной длины, а && и || — одинаковой. Важно когда пишешь ёлочку условий, с красивым форматированием оно оказывается выровненным друг с другом.
Так ли токсичен синтаксис Rust?