Как стать автором
Обновить

Комментарии 737

Cтатья на несколько тысяч знаков о токсичности синтаксиса <чиво-бы-то-ни-было> — это апофеоз развития ИТ-сообщества 21 века, которое мы заслужили.

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

Так же с удовольствием бы почитал о токсичности формы ручек для отверток, например.

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


Если исходный код выглядит как стенография лекции по истории гностических ересей в Южной Франции на палимпсесте конспекта курса по топологии, это не будет способствовать популярности такого ЯП на рынке труда: работодатели будут считать найм разработчиков такого кода слишком дорогим, а рабочих мест для работников не будет хватать.

Вы знакомы с, чтобы далеко не ходить, ABAP? Ни разу еще не встречал хронически безработного ABAPера. И ничего, работают, в основном, в командах.

С другой стороны, синтаксис 1С, не то, чтобы ужасен, конечно, нет — он чудовищен… А проблем таких на рынке нет.

Клингонский, по легенде, произошел из мертвого индейского языка, использовавшегося армейскими разведчиками США. Факт в том, что такие языки действительно использовались армейской разведкой: как раз для общения команды.

Тут налицо классическая дилемма: шашечки или ехать. К сожалению, молодые-перспективные разработчики 21 года 21 века настолько заигрались в шашечки, будь то новомодные новоязы, фреймворки, пайплайны, проценты покрытия тестами, прочие аджайлы или — омг — рейтинги токсичности синтаксиса, считая их чем-то действительно важным, что ехать уже становится проблематичным.

Почему молодые? Думаю, хейтеры синтаксиса Rust на Opennet вполне себе бородаты.

Молодость и перспективность как субъективное состояние души. Есть наверняка и 15-летки — апологеты Фортрана и Лиспа, и кто их осудит.

И да, само понятие «хейтер синтаксиса» вообще слабо коррелирует с опытом и мудростью, имхо.
Думаю, хейтеры синтаксиса Rust на Opennet вполне себе бородаты.

Ну я бородат. И мне не зашел синтаксис Rust. Правда на OpenNet я ничего не комментирую. Обстановочка не та.

Впрочем, я и не хейтер. Вопрос в том, что любой синтаксис становится привычным при регулярном использовании. Не в синтаксисе дело. Он явно создан сторонниками Pascal'я. Не хорош и не плох. Я поплевался пока пробовал Rust в деле, но явного отторжения не вызвал. Часть на С выглядит лучше, часть на Rust'е. Меня больше python выбешивал. Пока не плюнул и не сказал — не мое. И то ругаться не буду. Просто это не мой инструмент.

Вопросы к Rust они о другом. Впрочем, вот допишу статью потом обсудим… Лежит в черновиках…
«Раст» создан поклонниками «Паскаля»? Что-то не могу в «Расте» буквально ничего от «Паскаля» вспомнить.
Тип после имени переменной, а не до, как в C++ и Java.
ОМГ и это всё? А где «begin»/«end»? Присваивание через «:=», указатели через «крышечку»? На Си он в миллион раз больше похож, чем на Паскаль.

А что не как в TypeScript?

НЛО прилетело и опубликовало эту надпись здесь
Да почему сразу хейтеры Rust'a? Удобный синтаксис, если его изучать, уделяя не меньше времени чем остальным. Просто менее популярный, потому больше людей его хейтят т.к. не пользуются им на постоянной основе.
> Думаю, хейтеры синтаксиса Rust на Opennet вполне себе бородаты.
И не только на opennet, a вот я вчера побрился. И кстати как раз сегодня перед тем как спать пойти (после того как релиз залил), размышлял на тему об отсуствии статей на хабре об уродливости синтаксиса раста.
Целая куча замечательных, полезных и интересных идей так заштукатурена отвратительным уродливым синтаксисом, что на код даже смотреть не хочется. Дело не в сложности чтения этого кода, а в отвращении которое он вызывает при одном только взгляде.
Это что-то на уровне животных инстинктов, совершенно не рациональное, но это есть. Есть совершенно неполиткоректный факт, — многим людям сложно смотреть на калек и деформированных людей. Они даже взгляд отводят. Со временем это проходит, какая-то привычка вырабатывается. Так вот rust такое-же «биологически обусловленное» отторжение вызывает…

Это скорее узость кругозора. Когда-то и меня сильно волновал вопрос синтаксиса: отступы или скобочки, как эти скобочки расставлять, int* p или int *p и прочие "чрезвычайно важные нюансы". С опытом понимаешь, что всё это ерунда. Только опыт желательно иметь разноплановый. Скажем, пощупать лисп с хаскелем, а не только на С десять лет писать.

Не забывайте «пробелы или табы», главный философский вопрос ушедшего века!
НЛО прилетело и опубликовало эту надпись здесь

Да ну? hard_tabs = true. Пробелы по дефолту, но и всё. У нас например половина настроек форматирования отлична от дефолтов так-то.

Эта проблема, на самом деле, перестаёт выглядеть надуманной, когда открываешь код в редакторе с альтернативными настройками на количество пробелов. Типа какого-нибудь mcedit (а бывает, что приходится).
Табы плохой способ форматирования, хоть и терпимый.
Смешанный стиль — печаль...

А вы можете сформулировать более четкие претензии к синтаксису? Я понимаю ваши чувства, но не зная их подоплеку сложно спорить конструктивно.

В том то и дело, что чтобы чёткие претензии к синтаксису раста составить, нужен будет сеанс психоанализа. Я же написал, что это нечто иррацопнальное, не нравится и всё. Причём все идеи заложенные в раст очень нравятся, но смотреть на код неприятно.
Там вон уже и минусов понаставили, и в узости кругозора обвинили, а на самом деле это возможно из разряда фобий, или просто нечто социо-культурное. Вот как пример www.quora.com/Why-do-some-people-dislike-people-with-disabilities попытка объяснения. Но явление довольно массовое.
НЛО прилетело и опубликовало эту надпись здесь

Мне кажется, тут где-то уже спрашивали и вы что-то отвечали — но как эти символы вводить? Отдельную раскладку или IDE должна уметь в подстановку "tau" → "τ"?


UPD: нашёл, спасибо.

Для непосвящённых это выглядит как-то так:

По вашему синтаксис языка программирования не достоин обсуждения?

По-моему, употребление термина «токсичность», не относящееся к токсикологии и смежным темам — это красный флаг в целом.

А предметное обсуждение особенностей языков программирования — это круто и очень полезно.

А красный флаг тогда тут при чем? Мы разве гонки обсуждаем, или мореплавание, или революционную борьбу?

Красный флаг в том плане, что в прогрессивном 21 веке это слово вызывает аналогию с заменой ключевых слов в ЯП, чтобы чёрные не обижались (привет питон).
А что за ключевые слова в питоне заменили?

Так это же про документацию. Никто кейворды не трогал, к счастью.

Mat1lda употребил словосочетание «ключевые слова» не в том значении, которое поняли вы. Определение из википедии: «Ключевые слова — особо важные, общепонятные, ёмкие и показательные для отдельно взятой культуры слова в тексте»

Абсолютно согласен.


Вообще, обсуждение характеристик синтаксиса (даже если называть их уместными словами с соответствующими значениями, типа «читаемость») — мне непонятно в принципе. Совершенно очевидно, что любой язык (да, и кобол, и сиквел — тоже) превращается из нечитаемого в читаемый по мере погружения.


Когда я еду во Францию, я освежаю в голове всякие «сильвупле» и «мерси», а не говорю, мол, «фу, какой нечитаемый язык, я лучше пару дней буду питаться не в кабаках, а в супермаркетах без кассиров».


Какая вообще нахрен разница, каков синтаксис языка, если он не умеет, например, из коробки задействовать все ядра? А если умеет — ну в чужой монастырь не надо со своим синтаксисом лезть, можно потратить пару часиков на привыкание.

Какая вообще нахрен разница, каков синтаксис языка, если он не умеет, например, из коробки задействовать все ядра? А если умеет — ну в чужой монастырь не надо со своим синтаксисом лезть, можно потратить пару часиков на привыкание.

Пользователю языка — без разницы.
Разработчикам новых языков — полезно учиться на чужих ошибках, чтобы их не повторять.
Разработчикам новых языков — полезно учиться на чужих ошибках, чтобы их не повторять.

Это было бы круто, кабы существовала непротиворечивая корректная шкала оценки синтаксиса. А так, как ее нет, то и определить — ошибка ли это, или суперспособность — возможным не представляется. «Демократичным» голосованием это определять? — Увольте.


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


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

Тем и полезны статьи вроде этой, что каждый может ознакомиться с аргументами всех сторон, и сделать выводы самостоятельно.

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

Совершенно очевидно, что любой язык (да, и кобол, и сиквел — тоже) превращается из нечитаемого в читаемый по мере погружения.

Сложный декодинг определений типов C — как раз пример, что погружение помогает привыкнуть к тому, что "тут так принято" и смириться, но не тому, что всё равно придётся тратить каждый раз заметную порцию интеллектуальных усилий на его раскрутку.
С теми же фигурными скобками и фальшивым отступом аналогичный пример.

За что я люблю синтетические примеры, так это за всё.


int (*fns[16])(int * const * p)

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


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

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

И это, и что этот тип из себя представляет. В том и проблема. Пример реальный, а не синтетический.

А выражение «ядовитый тон» для вас тоже красным флагом является?
Словарь гласит, что «ядовитый тон» означает «презрительный».
Синтаксис ЯП может быть презрительным?

Вполне себе синтаксис можно презирать.

Речь же явно идёт о оттенке цвета.
Можно хоть один пример текста, где бы «ядовитый тон» означал оттенок цвета?
Создатели Rage 2 поиздевались над Ubisoft
Все из-за внешнего сходства Rage 2 и грядущего Far Cry: New Dawn — оба шутера выполнены в ядовитых тонах и посвящены постапокалипсису.

Любопытно: у сочетания «в ядовитых тонах» всегда цветовое значение, у сочетания «в ядовитом тоне» — всегда голосовое.
ну я думаю под «токсичностью» подразумевалась плохая читабельность и автор хотел внести этим какой то литературной красоты, эх какое словцо приловчил к тексту! А вообще код на Расте и правда трудно читать, особенно новичкам, не знаю, возможно просто привык к Си стилю(и терпеть не могу Perl). Но есть люди, которые высказывают мнение о плохо читаемости, и думаю стоит им пояснить почему создатели Раста решили сделать именно так, а не иначе, чтобы переманить часть тех, кого это не устроило в языке.

Вы саму статью читали? Хотя бы вводную ее часть?

"Токсичность" можно вольно транслировать как "недружелюбность". И недружелюбность чего-бы-то-ни-было, синтаксиса или ещё чего вполне возможна.

С первого дня изучения раста меня напрягали только лайфтаймы, всё остальное, плюс минус, уже видел в крестах.
Так же считаю ваши аргументы за первые 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, на котором он изначально и писался.
Там от ML рожки да ножки остались. До версии 1.0 можно было говорить про ML-подобный синтаксис, потом началась игра в «поддавки» C-подобному синтаксису.
НЛО прилетело и опубликовало эту надпись здесь
К сожалению оказалось, что от него совсем не просто избавиться: конструкция 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 массив, хоть хеш-таблица, хоть дженерик.
Rust может и значения элементов массива подставлять во время компиляции, несмотря на [].

Ну это все же относится к оптимизации частных случаев использования и не работает в общем случае.

НЛО прилетело и опубликовало эту надпись здесь
Можно пойти дальше и вообще все скобки, включая {}, заменить на круглые. Получится лисп :-)
О, привет Матлаб.
На деле же получается не очень удобно.

Можете рассказать, какие с этим возникают проблемы?

В принципе, никаких.
Но я не люблю синтаксис, завязанный на чуткое понимание контекста. То есть, когда есть миллион использований одних и тех же символов и в зависимости от контекста у них разное значение.
Хочется, чтобы контекст «берем 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» хоть при том, хоть при другом синтаксисе.
Как это не поможет?! — x, не определено, но понятно, что это имя функции -> IDE может сама мне сгенерировать «рыбу» определения функции.
В Rust можно инстанцировать Unit-структуры просто написав их имя

А зачем их как-то отдельно ещё и инстанцировать? Вообще сомнительная фича.

Затем, что они становятся бесполезными, если не иметь возможности их инстанциировать.

Они сами себе инстансы же.

Нет, их нужно инстанциировать, если необходимо получить объект данного типа. Как и другие структурные типы.

Ну например Result<(), Error> (функции типа записи в файл) требуют в конце Ok(()) чтобы компилировалось. Без инстанса () типа () тут не обойтись

Вопрос в том, зачем создавать больше одного инстанса каждого такого типа.

А как по-другому? Каждая функция возвращающая () должна создать свой экземпляр этого типа, иначе не работает. Это же не ГЦ язык где можно просто так ссылку вернуть, в расте ссылка должна была бы явно обозначаться &().


А ещё бывают массивы юнитов


let units: Vec<()> = xs.map(print).collect();

Все "инстансы" такого типа будут равны друг другу, так что нет никакого смысла "создавать" инстансы. Достаточно использовать имя типа в качестве алиаса для синглтона инстанса.

Стандартный ничем не отличается от энума 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 уж точно не нужна. Вот в раскладку клавиатуры новые скобки по-включать — идея по-лучше, жаль только нереализуема.

Ну есть в этом что-то логичное и правильное — «законодательно» разделить все символы на 3 группы: «международные», «национальные» и «дополнительные». И сделать так, чтобы «международные» кодировались 1 байтом даже в utf-8 (и заодно у производителей клавиатур появилась бы мотивация их наносить на клавиши), «национальные» укладывались в два байта utf-16 (кодовую плоскость, если я не ошибаюсь), а всякие эмодзи и древние алфавиты — всё остальное.
А первые 32 байта ASCII как раз не используются, кроме нуля, пробела, переносов строк и табов. Совместимость с древними телетайпами вряд ли кому нужна.
Вы из какого года пишете? Windows API до сих пор не поддерживает строки в UTF-8.

Ну, Windows API вместо этого UTF-16 использует. Невелика разница.


Однако, код программы напрямую в Windows API никто передавать не будет, а все нормальные текстовые редакторы UTF-8 таки поддерживают.

Про год лучше у вас спросить. Потому как в 2021м у большей части населения планеты Windows таки UTF-8 поддерживает. Только суровый Enterprise отстаёт.

Уточнение: поддерживать-то поддерживает, но баг в conhost до сих пор не закрыли. ReadFile из стандартного ввода не может прочитать русские буквы из консоли.

Потому что ReadFile заполняет буфер байтами, а не символами. Задача программиста конвертировать массив байтов в символы и строку.

В том-то и дело что там конвертировать просто нечего, ReadFile читает ерунду вместо строки в кодировке utf-8.

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

Другая сторона тут — conhost. И в нём есть баг, из-за которого он при активной странице 65001 пишет вместо русских букв в кодировке UTF-8 какую-то ерунду.

А программа которая для conhost поставляет данные, она знает, что должна конвертировать строку в байты в кодировке 65001 или по‐прежнему отдаёт их в 1251 и 866?

Нет никакой "программы которая для conhost поставляет данные", conhost как раз их и поставляет. По крайней мере, для стандартных консольных окон это верно.


Разумеется, conhost всё знает, и любые кодовые страницы кроме 65001 работают прекрасно. И 65001 тоже работала бы прекрасно если бы conhost выделял буфер по-больше. Где-то я читал, что он тупо не может преобразовать 1 символ в 3 байта потому что ему не хватает места.

Активная кодировка выбирается для всего процесса целиком, т.е. если в программе используется хоть одна сторонняя либа, то смена активной кодировки для процесса может её поломать. Много вы видели программ под Windows без использования сторонних либ? Т.е. поддержка «для галочки» появилась, но пользоваться ей на практике пока невозможно.
Под APL, где синтаксис состоял чуть менее, чем полностью из загадочных символов, делали специальные клавиатуры.

Если языки программирования массово будут использовать символы Юникода (как это уже делает Raku), то подтянутся и производители клавиатур.
Не, не подтянутся. Программисты это слишком малая часть пользователей клавиатур. Боюсь, уже поздно, а вот предлагаемая мной модификация Unicode, будь она сделана сразу в 1991 году, может и помогла бы.
Во-первых, конечно, любому современному сложному языку программирования не хватает спецсимволов.

Ох нет спасибо: я только сегодня возился с кодом на Xtend, в котором как часть синтаксиса используются ёлочки «». Часть исходников была в UTF-8, часть — в CP-1252, и используемая кодировка в самом файле никак не обозначена — «догадайся мол сама».
и предложил бы наносить их на все клавиатуры на буквенные клавиши (возможно потребовался бы еще один Shift)

Что делать тем, у кого на клавишах уже нет места для новых значков, а вокруг них уже нет места для новых Shift?

На большинстве клавиш по 3 символа. Вполне влезет и 4.
Но кстати, какие у них интересные есть символы — полускобочки двух видов, еще одни «панциреобразные» скобочки ❲ ❳
В принципе, такие бы всем непомешали, соответственно этим ребятам свободных клавиш понадобилось бы меньше:)
На всякий случай поясню, что 「...『...』...」— это кавычки (для прямой речи, названий и т.д.): обычные и вложенные; а 〔...〕 используются для вставки авторского пояснения в прямую речь.
Ну в программировании мы бы им обязательно нашли применение!
Какая разумная пунктуация в языке!
Я серьёзно.

Синтаксис с вертикальной чертой в качестве "или" довольно распространен в функциональных языках, откуда паттерн-матчинг и завезли в 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 => ...,
  _ => ...,
}
В D действительно правильно сделано. И в Rust этот подход кстати тоже применен — для атрибутов: символ # изменяет логику квадратных скобок
#[test]
fn test_foo() {
    /* ... */
}

А в C# сделано неправильно. Там есть оператор switch, его и нужно было расширять — разрешить ему возвращать значения, расширить синтаксис case-образцов и всё остальное, а они вместо этого взяли и ввели альтернативный синтаксис, совершенно ни на что не похожий. В конце концов, можно было бы кроме switch добавить еще match, в котором не требуется break после каждого case; но в остальном и switch и match должны обладать одинаковыми возможностями. Это логично, подобно тому как скажем циклы while и do..while существуют одновременно и отличаются только в одном аспекте.
НЛО прилетело и опубликовало эту надпись здесь
И что у него общего с оператором switch, кроме ключевого слова?
возможно потребовался бы еще один Shift
Эта проблема уже давно решена в странах, где пишут на польском и других странных языках с лишними буквами. Кстати, спецсимволов у них тоже там больше зачастую.
Однако на самом деле, предоставляемые Rust возможности требуют явного синтаксического разделения статических обращений xx::yy::zz и динамических xx.yy.zz, где результат будет зависеть от значения объектов xx, yy и zz во время выполнения.

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


Программа, написанная на Rust, не имеет информации о типах во время выполнения, так как при компиляции происходит "стирание типов".

Не вижу никакого "стирания типов" при компиляции. Автор явно понимает под этим словом что-то иное, не совпадающее со значением этого слова в Java или TypeScript.

Разве в Rust можно получить тип переменной во время исполнения? Завязав логику на этом.

При желании — да, см. трейт Any. При большем желании можно и что-то более хитрое придумать.


Однако, я бы не сводил Type Erasure к возможности получить тип переменной во время исполнения. Ну или, если всё же принять ваше определение, всё равно остаётся непонятной следующая связка:


Программа, написанная на Rust, не имеет информации о типах во время выполнения, так как при компиляции происходит "стирание типов". Поэтому возникает необходимость различать динамические и статические обращения: механизм их работы сильно отличается, как и последствия, к которым они приводят

Нет, я решительно не понимаю как невозможность узнать тип переменной в рантайме влияет на необходимость разделять операторы :: и .. К примеру, в Java используется только точка и ничего (да, первый оператор тоже есть, но он означает иное).

Действительно, в такой реализации, как у Rust, можно добавлять к типам метки как общее поведение и обращаться с ними динамически, но ведь можно и не добавлять. =) Data.Typeable в Haskell похожим образом работает, но про него говорится «types erased». В Typescript аналог в принципе что-то вроде function f(s: {typeLabel: string}), если ввести в стандарные библиотеки.

На самом деле мне удалось нагуглить вот какую проблему в параметричности 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.

Не вижу отличия от Java.

Где должны храниться значения статических полей в Rust, если после компиляции информация о типе нигде не хранится?

А какое отношение значения полей имеют к метаинформации?

Отредактировал свое сообщение.

Про скобки и отступы:
Из-за древнего примера из си предлагается ещё последующие 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 даже дальше парсить не надо: знаем, что там функция.

То, что вы описали, называется LL(1); контекстно-свободный парсинг — это намного более широкий класс.

Контекстная свобода — это независимость интерпретации от контекста. Пример контекста — число открытых скобочек. То же, о чём вы говорите, к контекстам не имеет отношения. Разные конструкции с общим префиксом — типичная ситуация. Например: const и continue.

Это одна и та же конструкция: в одном случае это будет Keyword("const"), а в другом — Keyword("continue"). Как будто вы никогда компиляторы не писали...

С тем же успехом можно утверждать, что let myFunc: (i32, i32) парсится в


[
    Keyword("let"),
    Name("myFunc"),
    Symbol(":"),
    Symbol("("),
    Name("i32"),
    Name("i32"),
    Symbol(")"),
]

Подобный парсер даже для подсветки синтаксиса не годится.

Именно в такой список и лексится :)
Парсеры в ЯП уже полвека как двухуровневые.
НЛО прилетело и опубликовало эту надпись здесь

По моему


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
}

Да! Но можно было показать проще:


fn take_equal_types<T>(_: T, _: T) {}

fn main() {
    take_equal_types(|| 10usize, || 10usize);
}
Предлагаю Boomburum ввести с Нового года такое правило, что статья со знаком вопроса в названии должна содержать опрос. Иначе это тухлый кликбейт, как здесь.
Пока что придётся довольствоваться опросом при нажатии на кнопку «минус», но там закрытый список вариантов.

Вы слышали когда-нибудь про риторические вопросы?

Меня всегда забавляло, что заголовки с ! в конце запрещены, но это не помешало мне пять лет назад запостить habr.com/ru/post/388505 поставив ZWNJ после !
НЛО прилетело и опубликовало эту надпись здесь
Я тут подумываю написать статью с рабочим названием «Что в доказательстве?»

Я чувствую рунглиш.

НЛО прилетело и опубликовало эту надпись здесь
qed и не-qed )
После вот этого:
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++

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

Независимо от того, делает ли строка message; хоть что-то или является скрытым дропом — ей не место в нормальном коде.

message; — просто частный пример для иллюстрации того, что происходит. В реальном коде будет foo(); с дропом в этом месте значения, которое вернула foo, который может приводить к побочным эффектом, в случае кастомизации Drop'а этого значения.

Но если там будет foo(message); с дропом этого значения, то никак не вина языка в том, что компилятор отловил ошибку. Именно поэтому хейтеры и приводят искусственные примеры с message;, которые в реальном коде никогда не встретятся...

Ну это всего лишь ваше мнение, я прекрасно могу представить что такой код может появиться в раст для оптимизации, например закрыть файл раньше чем завершится скоуп где обьявленна файловая переменная. drop(file); банально сложнее набирать. У меня лично персональная ненависть к скобкам и я бы точно предпочел file;

Зато проще читать. А вот у меня бы точно возник бы вопрос, что делает такая сиротливая строчка и не нужно ли ее удалить.

НЛО прилетело и опубликовало эту надпись здесь

Я точно знаю, что вы отлично разбираетесь в с++, но в вашем примере я аналог message; не нашел вместо этого вы иллюстрируете побочный эффект мува что совершенно точно не тоже самое. Я думаю вы просто уже подзабыли из за долгово неиспользования с++ что единственная разумная причина такого кода на с++ это подавление ворнинга о неиспользуемой переменной. Есть 100500 макросов которые генерируют именно такой код — переменная; #define unused(x) x; Смысл в том что семантика раста в этом случае это дестрактив мув которого нет в с++, но это и есть проблема т.к. в с это просто ничего.

НЛО прилетело и опубликовало эту надпись здесь

есть такой атрибут [[nodiscard]]. И вот какой нибудь очень талантливый человек решил, что он точно знает, что вот ни в коем случае нельзя игнорировать возвращаемое значение, но вы с ним почему то не согласны. Как предлагаете решать такое противоречие в проекте где ворнинги это ошибка? Что насчёт условной компиляции, где для одной платформы переменная используется, а для другой — нет?

#pragma GCC diagnostic ignored "-Wwhatever"

а что насчёт MSVC/clang? у меня на работе все три в одном проекте используются. Да, конечно, это решение, но сравните сколько усилий требуется, что бы заглушить варнинг для 3 компиляторов с помощью прагм и просто написав имя переменной с точкой запятой.

Так и пример с точкой запятой немного надуманный, на практике будет 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 экрана, то я задумаюсь о выделении в отдельную функцию (почти наверняка) или добавлю ключевое слово (хотя наверняка это будет из-за желания уменьшить количество отступов в первую очередь). Но вот говорю как есть — пока такого в моей практике не встречалось.

Соглашусь, что иногда return удобно использовать, чтобы сократить количество вложенных блоков и улучшить читаемость. Хотя во многих других случаях лучше без него.

Ну в теории, с return ты ищешь ключевое слово return, а в случае в растом надо весь код читать, смотреть где есть if else, match и и тп. а если там в качестве выражения цепочка filter map find так глаз еще и замылиться может, есть ли там вобще точка с запятой.

Как человек, который пишет на 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 года назад
НЛО прилетело и опубликовало эту надпись здесь

Да и в switch штука полезная будет.

как понять что метод что-то НЕ возвращает

Смотреть на сигнатуру. В любом случае лучше всегда на нее смотреть, зачастую этого уже достаточно.

я пока так из этой ветки не понял как понять что метод что-то НЕ возвращает, мне смотреть на сигнатуру или искать глазами точку с запятой?

На сигнатуру, конечно, типы не врут.

код на 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 человек с плохим пониманием понимает, что там происходит куда лучше, чем у большинства языков.

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

По отзывам, тайпхинты в Python (e.g. def run(input: Input) -> bool) начинают проставлять для более простого вхождения в код разработчиков со стороны. То есть типы используются исключительно, чтобы улучшить читаемость, ни о какой корректности пока не ведётся речь (но когда везде проставлены, то нет смысла не включать анализатор).

О, то есть мы сначала объявляем типы ненужными и проверяем их в рантайме, а потом таки ставим хинты, которые ни на что не влияют и которые нужно проверять сторонним инструментом! Фантастическая рациональность.

Посмотрите, когда появились все такие ЯП. В 90-х годах была восторженная уверенность, что динамические интерпретируемые языки упростят разработку, т.к. о типах больше не надо будет задумываться. Роберт Мартин с тех и до сих пор на таком настаивает, с юнит-тестами для гарантий контрактов. Но индустрия в этом, по-видимому, уже разочаровалась и двигается в другом направлении.
Хинты появились в питоне в 2010-х, хотя мышление основной массы разработчиков пока инертно. Но, тем не менее, вышеупомянутая мотивация имеет место.

Речь о том, что сначала объявили типы "устаревшими", которые "ограничивают программиста", а потом в течение пары десятков лет наткыкались на детские "внезапно вместо числа пришла строка", которые сначала лечатся какими-нибудь дикими правилами приведения, а потом выработали практики как этого не допустить, и в итоге приходим к тому, что типы-то оказывается не такие уж бесполезные были.

По моему опыту проблемы «внезапно вместо числа пришла строка» в реальной практике не существует. 80% проблем — это плохая работа с Option/Maybe. При этом оно встроено в дизайн некоторых ЯП неисправимым образом (Java, C#). Поэтому с динамическими ЯП, где статанализ навешан сверху и сильно потом, мне было в каком-то роде проще роде работать — основная проблема не была повторена.
Потом идут последствия рефакторинга — IDE практически беспомощны без тайпхинтов, надежда после любого изменения контрактов только на полноту юнит-тестов, которая не может быть формально гарантирована.
Далее следуют ошибки, когда разработчик опирается на интерфейс структуры без каких-либо оснований, например, ошибки динамического/ad-hoc диспатча, недоконвертация внешних данных во внутренние структуры, и т.п.

Про то, что можно словить проблемы при type juggling я знаю только теоретически. Да, практики порешали, но эти практики (в основном hexagonal arch & clean code) имеют смысл и безотносительно типов.
По моему опыту проблемы «внезапно вместо числа пришла строка» в реальной практике не существует.

Да-да, именно поэтому если к какой-либо ЖС либе нет документации то воспользоваться ей тупо невозможно. Сидишь по сорцам пытаешься понять, какую форму должен иметь объект. И оказывается, что если вместо 12 передавать "12" то всё чудесным образом начинает работать.

Ну ок, у нас разный опыт и разная статистика. Я, честно говоря, не могу писать код на Javascript, не хватает интеллектуальных способностей и терпения. Поэтому в динамических ЯП существенно опираюсь на PHP, Python и в меньшей степени Javascript.

типы в IDE покажут что это за объект, дадут автокомплит по методам) в PHP тоже нету проверки во время компиляции. Это интерпретируемые языки. исключение составляет тайпскрипт, который является историческим костылём над тем, что уже все используют и поэтому трудно это поменять

Когда везде типы проставлены, как раз самое время включить анализатор! mypy/pyre/whatever в этот момент становится еще одним этапом сборки, как форматтинг, юнит-тесты и линтеры. Единственная претензия к языку в этой истории — перечисления работают за счет магииметаклассов, а не являются частью синтаксиса, поэтому никаких Result/Option как в Rust.
перечисления работают за счет магииметаклассов, а не являются частью синтаксиса, поэтому никаких Result/Option как в Rust

Нет, не поэтому.

Не, enum из Python никакого отношения к типизации не имеет и созданы они давно, чтобы делать проверки в динамике. В Python ближайшая замена перечислений из Rust как типа-суммы — это Union. Соответственно, для Option есть тёзка: Option[T] = Union[None, T]. Для Result это будет Union[T, Err[E]], можно иначе, но в любом случае как-то так.

Если не хотите думать, что правильно, то можно использовать github.com/dry-python/returns

во-первых && и || привычнее родному сишному глазу
во-вторых все остальные операторы std::ops именно что операторы. Делать исключение для булеанов, да при том что & и | как бинарные операторы на тех же булках никуда не денешь, хотя бы ради сокращенной записи x &= bar; y |= baz, наверное было бы плохой идеей. Тем более что or/and различной длины, а && и || — одинаковой. Важно когда пишешь ёлочку условий, с красивым форматированием оно оказывается выровненным друг с другом.

Вполне возможно иметь и &&/||, и and/or, как в PHP и в Perl.

Так в расте так и работает для всех операторов, кроме именно этих. Комментарий в документации объясняет, почему так:


Note that the && and || operators short-circuit, i.e., they only evaluate their second operand if it contributes to the result. Since this behavior is not enforceable by traits, && and || are not supported as overloadable operators.
Since this behavior is not enforceable by traits
Может быть все равно enforceable. Пусть трейт принимает замыкание и не вычисляет его если не нужно.

Эдак можно получить вместо ускорения замедление, если замыкание почему-то не захочет инлайниться. Лучше уж поступить как в C#, где для перегрузки && и || есть operator true и operator false. Там выражение a && b вычисляется как a.false() ? a : a&b, ну а a || b — соответственно как a.true() ? a : a|b.

НЛО прилетело и опубликовало эту надпись здесь

Семантически они странные. Что такое "ИИ" "ИЛИИЛИ"? В Си оно имело смысл в рамках локального птичьего языка (++a+++++b--), в Rust осталась только += форма (как более разумная), так что сделать && и || в and и or было бы разумно. Заодно было бы понятно, что это не арифметические выражения, а логические. То же касается и not. Минус омонимия (с макросами и divergence в типах), плюс читаемость, плюс явное указание на то, что "это не оверлоадится".

ИИ\ИЛИИЛИ — это сокращённые формы И/ИЛИ, работающие с short circuit, к которым за время существования С все привыкли

Все — это кто? Люди, привыкшие, что у них UB на каждой второй строчке? При всём уважении, так себе аргумент. Они же привыкли ++ писать и кастить void* к нужному типу из malloc'а.


and, or и not как указание на логику, а не биты — очень разумно. Замечу, что '!' в расте зачем-то перегружаемый и вызывает wtf, потому что


let x: bool = !true — это false,
а let x: u8 = !1 — это 0xFE


А true! — это макрос.


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

С++/C#/Java/JS — Достаточный перечень языков, чтобы выбрать такую запись.

Так в этих языках разделение как раз имеется: ! — это логическое отрицание, ~ — побитовое дополнение. Отличие же от Раста в том, что в Расте строгая типизация — нет неявного приведения произвольных выражений к логическому типу, поэтому и нет необходимости в двух разных операторах.

Представьте себе кастомный тип в котором необходимо поддержать обе операции: и отрицание, и дополнение. Например — значение четырёхзначной логики. Подход, где на один оператор навешивается несколько разных семантик этому, очевидно, мешает.

Вообще, если рассматривать bool как u1, то никаких разных семантик нет. Кроме того, подход, когда операторов слишком много, делает изучение языка и чтение программ ещё сложнее.


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

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

Писать полезно, читать уже не очень. Читаемость кода — это важно. Иногда даже важнее, чем удобство писателя.

Если это какие-нибудь графы, то с названиями методов типа addEdge или closure читать проще, чем догадываться, что автор имел в виду под бинарными операторами +:, +++ и ~>, которые даже не погуглишь.
НЛО прилетело и опубликовало эту надпись здесь

Зависит от того, что подразумевается под звёздочкой и сложением. Умножение или что-то другое?

Да обычное сложение и умножение. На матрицах, например.

НЛО прилетело и опубликовало эту надпись здесь

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


На самом деле не нужны операторы, нужна возможность:


  1. писать функции в инфиксном стиле
  2. возможность именовать функции не только словами, но и символами.

В хаскелле иногда например вместо fmap foo bar пишут foo 'fmap' bar. В том же расте это решают всякими хелпер-трейтами типа FooBarExt чтобы можно было через точку писать имя своей функи, но какой-то более общий механизм был бы кстати.


А так да, умножения могут быть разные, да и кроме арифметических операций могут быть другие, например ask/tell на акторах.

Уточнение: foo `fmap` bar

Я пишу в маркдаун-стиле, а там одинарные кавычки это начало блока кода. А проверять эксейпинг хабра как-то не хочется, у него всегда проблемы с подомными вещами.

Я тоже в маркдауне писал.

Не вижу что вы пытаетесь писать в маркдауне, но судя по DOM там явно что-то странное. Правильная разметка — вот такая:


``foo `fmap` bar``

Это не только на хабре так.

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

bool вообще‐то уже хранится как u1, с довеском из семи бит, всегда равных нулю, существующим исключительно по технических причинам. А вот единичную систему счисления для чисел почему‐то не используют.

НЛО прилетело и опубликовало эту надпись здесь

А где-то бул и как 4(8) байт хранится

На архитектурах, где чтение байта с адреса, не кратного слову, медленнее — это вполне логично.

Я только что написал в своём пет-проекте контр-пример, который мне очень не понравится.


assert!(!area_one.overlaps(area_two));

Вот этот assert!(! очень шумный.

Если оно выделено разными цветами, то вообще не напрягает.

Вы не можете иметь подсветку синтакиса всюду. Визуально перегрузка восклицательного знака — шумно. Я вообще не могу понять что имели в виду '!' для макроса. Что оно требует внимания? Почему? Для '!' как неравенства есть хотя бы визуальная аллюзия на ¬, да и! используется не только в языках программирования (вроде бы).


А для макроса-то за что?

Предложите свой символ для макроса, чтобы было видно, что это макрос, а не просто функция

Ага, вон в C/C++ макросы вообще никак не отличаются от обычных идентификаторов и не имеют ограчений по области видимости. А потом приходится искать причины, почему это проект не компилируется после подключения какой-нибудь библиотеки (и это касается даже стандартной библиотеки).

НЛО прилетело и опубликовало эту надпись здесь
Вот обрадуются-то пехапешники
НЛО прилетело и опубликовало эту надпись здесь

Моё чувство прекрасного говорит, что из всех символов с клавиатуры, в Rust меньше всего заезжен '@' и '%'. % в синтакисе всегда очень некрасивый (привет, постскрипт), так что, видимо, @. В начале или в конце — это вопрос интересный.


Ниже ещё '$' предлагают. $print(f"{array[2]}") не самый плохой вариант. Или print$. Хотя, в целом, значков на клавиатуре не хватает.


print※ выглядит красивее всего, imho. Вообще, в японском такой волшебный разгул красивых значков...


https://en.wikipedia.org/wiki/List_of_Japanese_typographic_symbols

А по мне, так очень оторванно выглядит. Не воспринимается, как единое целое с идентификатором

Доллар уже используется внутри макроса для обозначения метапеременных, будет путаница. @ во-первых не подходит семантически, во-вторых точно так же будете ругаться:


assert@(match x { Foo(bar @ y) => true, _ => false }

print※ выглядит красивее всего, imho. Вообще, в японском такой волшебный разгул красивых значков...

А что делать людям без японской клавиатуры? Разбираться как это набирается?

Это главная беда современного программирования. Значков мало. Математики себе изобретают, а программисты страдают.


Посмотрите по ссылке на красоту и вариативность скобок...

Мило, но меня трудно впечатлить греческими буквами (на Кипре это штатная раскладка).

Греческих букв на этой клавиатуре всего четыре.
Ни разу не видел ! для отрицания вне языков программирования: где не ¬, там черта сверху. Меня как раз удивляет, почему K&R выбрали !: визуального сходства с ¬ я не вижу даже близко.

Ну не знаю. Во многих старых языках программирования для отрицания равенства использовался оператор <>, а не !=.

НЛО прилетело и опубликовало эту надпись здесь

Зачем нужны отдельно not и побитовое отрицание, если ! либо будет делать то же, что и not, либо его нельзя будет применить к булевым, а not в любом случае нельзя применить ко не‐булевым? && и || — это особый случай из‐за short-circuit и, соответственно, необходимости особого отношения со стороны компилятора.

Очевидно, что основная проблема раста: отсуствие поддержки четырёхзначной логики.

Четырёхзначная логика элементарно реализуется на структурах в любом языке.

И после этого вы ещё что-то говорите про лаконичность. Писать 3 строки вместо 1 — это верх расточительства. С отступами было бы 2 — разумный компромис между лаконичностью и дебагом/трейсами.

Мне напомнить про goto fail баг? В Rust допустить подобную ошибку было бы проблематичнее просто в силу обязательности скобок вокруг тела.


А ещё после Rust обязательные круглые скобки вокруг условия в других языках бесят.


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

Случайно это сделать сложно, у вас иначе просто код не скомпилируется из-за несовпадения типов.

Иногда let ругают за его избыточность и ненужность. Но обычно это делают те, кто не идет дальше элементарных примеров объявления переменных в Rust. Дело в том, что инструкция let занимается не объявлением переменных, а сопоставлением выражения с образцом (паттерном):

А если сделать без let? Что будет хуже?

Невозможно будет отличить случай сопоставления от случая присваивания. Тогда придется выбрать иной синтаксис для сопоставления, скажем, такой:


a @= 25;

вместо


let a = 25;

Вариант c let выглядит чище и традиционнее. И он как бы намекает, что перед нами все-таки не присваивание (раз он присутствует). Но многие незнакомые с языком думают, что тут именно присваивание, и что поэтому let лишний.

Невозможно будет отличить случай сопоставления от случая присваивания.

Зачем их отличать?

чтобы запись x = 5 не была контекстно-зависимой. А ещё вам в любом случае нужен синтаксис для паттерн-матчинга.

В JavaScript как-то обошлись. Ну или в чём принципиальное отличие деструктуризации от паттерн-матчинга? Почему для деструктуризации использование знака равенства не вызвало «контекстной зависимости», а для паттерн-матчинга — вызвало бы?

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

Если у вас аллергия на JavaScript, возьмите Python: там тоже синтаксис деструктуризации и присваивания совпадает.

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

Дело не в деструктуризации и присваивании, а в объявлении и присваивании. И нет, не могу назвать их совпадение в Питоне достоинством Питона.

ОК, мотивация «хотим явного объявления переменных» намного яснее, чем названная PsyHaSTe «в любом случае нужен синтаксис для паттерн-матчинга.»
НЛО прилетело и опубликовало эту надпись здесь
Хочется читать код, а не разгадывать головоломки...

Есть ровно один способ этого добиться: сквозь границу скоупа переменная может просочиться в двух случаях: кложурный захват и макрос с явно объявленной негигиеничной переменной.


Мутабельной переменной, видной отовсюду, чуть ли не $global — не поможешь синтаксисом, это родовая травма языка. И все эти отмазки про производительность и (хахаха) циклы — это детский сад. Если уж так надо дать возможность стрелять в ногу, нужно не let обсуждать, а сделать способ эксплицитно отменять гигиену скоупа, типа


int n = 0;
while (n < 42) {
    ext!(n)  = process(n);
}

Сий макрос должен орать из кода: то, что вы тут наменяете, просочится во внешний мир.

Вот я тоже не понимаю почему люди нуждаются в разделении объявления и присваивания. Если вас не затруднит можете обьяснить почему вы считаете что на концептуальном уровне присваивание не тоже самое что инициализация и одновременное сокрытие переменной с тем же именем объявленной ранее. Я считаю что разницы нет на КОНЦЕПТУАЛЬНОМ уровне, а тот факт что есть разница для ЯП это просто напросто "узаконенная" оптимизация. Эта операция происходит настолько часто что очень важно что бы она работала быстро, поэтому и происходит разделение позволяющее оптимизировать присваивание. Также я считаю, что компилятор мог бы и сам различать когда это оптимизация применима, а когда нет имея единый синтаксис объявления который выглядит как присваивание. Имея два синтаксиса компилятор перекладывает на меня бремя по идентификации мест где эта оптимизация применима. 50 лет назад это решение было рациональным — сейчас нет.

На концептуальном уровне разница появляется когда становится важно в блоке какого уровня переменная объявлена. Хочется читать код, а не разгадывать головоломки...

Я понимаю что не так то просто привести хороший пример, но прошу вас постарайтесь. Я думаю вы знакомы с IIFE идиомой которая на с++ реализуется через лямбду. Т.е. ЛЮБОЙ блок кода заворачиваем в лямбду и тут же ее вызываем. Внутри лямбды скрываем/переопределяем переменную из контекста вызова и вуаля компилятор/программист счастлив — оба не против и не ругаются что в лямбде локальная переменная имеет тоже имя что и в вызывающем коде, но может быть другого типа и/или иметь другое значение.

Если убрать трюк с лямбдой то оба компилятор и конвенционный программист на с++ начинают выть что это ОЧЕНЬ плохо когда локальную переменную скрывают/подменяют в этом куске/блоке/скоупе, т.к
становится важно в блоке какого уровня переменная объявлена. Хочется читать код, а не разгадывать головоломки..
Опять же я хочу обратить ваше внимание на тот факт, что тот же самый код вместе с IIFE НЕ вызывает проблем (хотя я лично ваше мнение ещё не знаю, может для вас и вызывает).

Я специально так делаю на работе уже довольно продолжительное время и заметил что это не вызывает РЕАЛЬНЫХ(мне не известны случаи что бы это привело к багу или жалобе что читать такое сложно) проблем с пониманием у моих коллег. Мало того некоторые спустя несколько месяцев начинают делать так же.
Если убрать трюк с лямбдой то оба компилятор и конвенционный программист на с++ начинают выть что это ОЧЕНЬ плохо когда локальную переменную скрывают/подменяют в этом куске/блоке/скоупе, т.к

Ну вот а в сишарпе шедовинг и переопределение встречаются на каждом шагу. Например, можно посчитать, сколько различных значений path/db есть в примере ниже:
img


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

В вашем примере есть два типа объявления (неизменямый и изменяемый биндинг), но нет ни одного присваивания. Есть мутация, но она имеет совсем другой синтаксис — вызов функции.

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

Ну как раз в 99% случаев я и не мутирую ничего, а только объявляю. Но перепутать эти 99% с 1% мутаирования существующей переменной я бы не захотел.


let дает однозначную грамматику, однозначная грамматика дает лучшую работу IDE, лучшие подсказки, лучшие ошибки и так далее. Хотя бы ради этого смысл в нем есть. Разница в том что если я пишу:


systemuserinfomask = 0xff;
for x in xs {
  if cond {
    systemuserinfmask &= x;
    debug("New mask is {}", systemuserinfmask);
  }
}

То я хочу видет ошибку компиляции (нашли, в чем проблема?)

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

По мотивам вашего примера можно представить такой код
mask = 0xff;
for x in xs {
  if cond {
    mask = mask & x;
    debug("New mask is {}", mask);
  }
}


Да в моем мире такой код будет иметь иную семантику т.к. mask
в if скоупе будет локальной перменной и на каждом проходе цикла будет битово эндится с неизмненной mask объявленной до цикла. Т.е. в моей вселенной такой пример будет достигаться следующим кодом на расте
mask = 0xff;
for x in xs {
  if cond {
    let localMask = mask & x;
    debug("New mask is {}", localMask);
  }
}


Что бы написать код в моей вселенной с такой же семантикой как у раста придется писать что то типа

mask = 0xff;
mask = for x in xs {
 mask = if cond {
    mask = mask & x; // нет, компилятор не спутает правый mask с левым в этом объявлении - рекурсивного объявления не будет
    debug("New mask is {}", mask);
    mask
  } else { mask }
  // если нужно пробросить локальную версию mask на родительский скоуп то нужно явно об этом попросить и соотвественно в том скоупе явно ПЕРЕОПРЕДЕЛИТЬ
  mask
}


Конечно вы можете сказать, что я сошел сума и так писать не удобно/не правильно и т.д., но это всего лишь ваши привычки. Я лишь показываю что нет НИКАКОЙ концептуальной разницы между присваиванием и объявлением, а все гипотетические сложности которые вы здесь приводите это следствие побочных эффектов которые уже давным давно побежденны в фукнциональных языках. И мне кстати в связи с этим очень странно видеть 0xd34df00d на одной стороне с вами по этому вопросу. Мало того в моей вселенной я могу читать любой код вырванный из любого контекста и ТОЧНО понимать что в нем происходит, а в расте(да что уж почти во всех языках) если я вырву кусок кода из контекста (схлопну цикл в IDE) то я понятия не имею что внутри цикла мой mask меняется — это ИМХО затрудняет понимание кода — так называемая локальность кода/семантики нарушается. Мне нужно видеть больше чем минимум что бы адекватно мыслить о том как этот код работает.

Речь о том, что семантика раста максимально контекстно-независима.


В вашем случае если мы видим mask = 12 нужно идти вверх по скопу смотреть "а нет ли там какого-нибудь подходящего mask`? Причем если выше по скоупу есть, но другого типа (например, String), то компилятор что должен выдавать? Мне ни ошибка не нравится, ни отсутствие её в этом случае :)


Поэтому леты и ещё раз леты. Тем более, что как выше было замечено это консинстентно работает с паттернами if let/while let/...

Вот рабочий код:
fn main() {
    let systemuserinfomask = 0xff;
    for &x in [0,1,2,3].iter() {
      if true {
        let systemuserinfmask = if x == 0 { systemuserinfomask } else { systemuserinfomask & x };
        println!("New mask is {}", systemuserinfmask);
      }
    }
}

Он компилируется. Чтобы его скомпилировать, мне пришлось добавить let. Если я убираю let, то получаю
cannot find value `systemuserinfmask` in this scope


Идеология Rust — максимальная безопасность. Принятое решение на 100% соответствует его идеологии. Кроме того, никакие из современных ЯП не разменивали эту существенную толику безопасности на экономию трёх символов, не правда ли? Явное лучше неявного и т.п.

Да нормальный рабочий код, только мне опять не понятно зачем здесь лет. Он НИЧЕМ не помогает ни мне ни компилятору. Уберите его и просто посмотрите на код вы никапли не потеряете в читабельности. Ваш пример имел бы больший вес в этой дискуссии если бы второй лет хайдил первый лет. Но как я уже писал выше это была бы локальная переменная изменение которой не влияет на выше объявленную если вы явно это не укажите в двух местах. Если вы посмотрите на мой пример, то увидите что то о чем я говорю приводит к ещё более явному коду чем то что сейчас в расте — чтобы поменять значение переменной верхнего скоупа нужно явно вернуть значение из вложенного скоупа и переопределить переменную верхнего скоупа, тогда как сейчас в расте и других не функциональных языках достаточно внутри цикла присвоить новое значение не локальной переменной == воспользоваться побочным эффектом. По сути то о чем я говорю технически запрещает часть побочных эффектов связанных с присваиванием. Ещё раз, замена присваивания на переопределение технически устраняет часть побочных эффектов как невыразимую тем самым уменьшая возможность ошибиться, но меняет семантику делая ее не ПРИВЫЧНОЙ для тех кто привык использовать побочные эффекты — почти всех программистов на земле. Да я вижу что в силу привычек вы не можете увидеть сейчас преимущества, но для меня оно совершенно очевидно. Я уверен, что мы с вами застанем новые промышленные языки которые сделают присваивание не выразимым, только (пере)определение.

к ФП отношение имеет чуть более чем никакое. В хаскелле вы в любом ифе можете сделать writeSTRef и получить ровно то же поведение.


То что вы пишете выше — вы предлагает ввести неоднозначную грамматику где a = b может иметь разный смысл в зависимости от того, есть ли где-то выше по скоупу такое a или нет. Это тяжело парсить парсером -> с этим тяжело работать человеку -> не нужно.


Да я вижу что в силу привычек вы не можете увидеть сейчас преимущества

Могу вернуть вам ту же аргументацию.

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

Ваши б слова да Гвидо в уши:
>>> def test(xs):
...   systemuserinfomask = 0xff
...   def mask_with(x):
...     systemuserinfomask &= x
...     print("New mask is ", systemuserinfomask)
...   for x in xs:
...     if x%2:
...       mask_with(x)
...
>>> test([1])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 8, in test
  File "<stdin>", line 4, in mask_with
UnboundLocalError: local variable 'systemuserinfomask' referenced before assignment

Опечатки нет, но &= всё равно истолковалось как объявление.

А как же костыль nonlocal?

Ну судя по синтаксису и моим куцим знаниям это питон. Худо-бедно, но об ошибке рапортует. Лучше хоть как то работающий ЯП чем идеальный в вакууме голове дизайнера языка.
почему объявление нужно отличать от присваивания

Иногда удобно для уменьшения сползания вправо, ну и код красивее выглядит.


let x;
if cond1 {
  x=12;
} else {
  return;
};

вместо


let x=if cond1 {
    12
  } else {
    return;
  };
Очень интересно как вы выдаёте пример раздельного ОБЪЯВЛЕНИЯ за пример присваивания. let x; НЕ является законченным объявлением, но образует объявление вместе с последующим синтаксисом присваивания.

А это тогда раздельное объявление, которое становится целым только в рантайме?


let x;
if cond {
  x=12;
} else {
  x=13;
};
нет, оно становится цельным во время компиляции, замените 12 или 13 на строковый литерал или на не целое число и получите ошибку что тип переменной нельзя однозначно вывести.

Да. Я уже разобрался, что вы хотели сказать. Это называется static single assignment form.


Можете попытаться развернуть в такую форму код с массивами.


const N=10;
let mut a[N] = [1; N];
for i in 0..100 {
  a[a[i%N]%N] += 1;
}
Если вас не затруднит можете обьяснить почему вы считаете что на концептуальном уровне присваивание не тоже самое что инициализация и одновременное сокрытие переменной с тем же именем объявленной ранее.

int n = 0;
while (n < 42) {
    n = process(n);
}

Здесь вы никак не замените присваивание внутри цикла на сокрытие и переинициализацию переменной.
любой цикл может быть заменен на рекурсию (цикл частный случай рекурсии)
void recursion(int n = 0)
{
    if (n >= 42) return;
    recursion(process(n));
}

Когда то давно я восхищался языком Nemerle в котором циклов не было принципиально — только рекурсия, но при этом были макросы которые выгледели как циклы и заменяли их на рекурсию, и да потом оптимизатор заменял левую рекурсию снова на циклы. И во всем этом жонглировании есть смысл — сайд эффектом было то что любая левая рекурсия в коде сама заменалсь на циклы, т.к. авторы потратили время на оптимизацию рекурсий.
«Без присваивания можно обойтись, если переписать весь код в стиле Nemerle» — не то же самое, что «без присваивания можно обойтись, это просто оптимизация сокрытия с переинициализацией».
Ну, а теперь пожалуйста если не затруднит, покажи мне на КОНЦЕПТУАЛЬНОМ уровне в чем же разница. Если это действительно так, то должен существовать простой, часто используемый шаблонный контр пример, который с лёгкостью демонстрирует разницу.

Я еще раз уточню — присваивание это пере использование ресурсов аллоцированных/ассоциированных с именем для другого «значение» что бы это(значение) не значило.

Объявление это ассоциация/аллокация ресурсов (значения) с именем.

Когда вы присваиваете что то левому операнду, то правый уже существует. Я думаю вы согласитесь что на концептуальном уровне
auto value = Type{xxx};
auto anotherValue = Type{yyy};
value = move(anotherValue);
use(value);

тоже самое что и
auto value = Type{yyy};
use(value);


Я не могу представить программу где бы была невозможна замена ВСЕХ присваиваний на объявления и сохранение поведения программы кроме эффективности использования ресурсов.

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

Я не могу представить программу где бы была невозможна замена ВСЕХ присваиваний на объявления и сохранение поведения программы кроме эффективности использования ресурсов.

Я же выше привёл простой, конкретный пример.
Циклы очень часто используются, хотя код можно было бы писать и без них.
Ну я же уже продемонстрировал, что цикл с побочным эффектом на внешней по отношению к скоупу цикла переменной легко выражается/заменяется на цикл/рекурсию где НЕТ побочных эффектов для нелокальной для цикла переменной.

Я прошу привести пример, где никак невозможно заменить присваивание на переопределение + явное вынесение побочных эффектов (если они есть).
Покажите замену того кода на цикл без присваиваний. Без рекурсии.
Я же выше привёл простой, конкретный пример.
Циклы очень часто используются [...]
int n = 0;
while (n < 42) {
  n = process(n);
}

Во-первых, есть куча языков, в которых циклы в принципе не предусмотрены синтаксисом. Во-вторых, в мультипарадигменных языках как-то все более принято заменять циклы на функциональные аналоги типа map/reduce, или, на худой конец, comprehensions. В-третьих, даже в языках с «родными» циклами, вышеприведенный фрагмент — смердит, и я лично разворачиваю на ревью код, который так волюнтаристски мутирует весь окружающий мир; это нечитаемо и очень подвержено привнесению ошибок впоследствии.


Покажите замену того кода на цикл без присваиваний. Без рекурсии.

А почему она должна существовать? «Покажите замену кода с goto на код без goto и без циклов» — тоже разновидность некорректного безответного вопроса.

даже в языках с «родными» циклами, вышеприведенный фрагмент — смердит, и я лично разворачиваю на ревью код, который так волюнтаристски мутирует весь окружающий мир; это нечитаемо и очень подвержено привнесению ошибок впоследствии.

Но это же стандартная реализация поиска в дереве: как process я обозначил выбор ребёнка, как сравнение с 42 — сравнение ключей.
Или своёв std::collections такое не смердит?

А почему она должна существовать? «Покажите замену кода с goto на код без goto и без циклов» — тоже разновидность некорректного безответного вопроса.

Я уже ответил на пять каментов выше в ветке.
Но это же стандартная реализация поиска в дереве [...]

А ничё, что код, на который вы дали ссылку — уже рекурсивен? Подозреваю, что Rust не сможет вывести тотальность перекрестной рекурсии, поэтому там и появляется хак с псевдобесконечным циклом, переопределением биндинга из внутреннего скоупа и бесчисленными ранними возвратами, превращающими и без того лапшу — в мелкопокрошенное спагетти. Может быть, лучше заняться доведением до ума компилятора, чтобы можно было бы избежать вот таких хаков?


Я уже ответил на пять каментов выше в ветке.

Ну это ответ в духе «все ваши варианты мне не нравятся, поэтому не существуют».

А ничё, что код, на который вы дали ссылку — уже рекурсивен?

Покажите мне (перекрёстно-)рекурсивный вызов search_tree, а то я его в упор не вижу.

Ну это ответ в духе «все ваши варианты мне не нравятся, поэтому не существуют».

Вы пытаетесь меня убедить в существовании языков без циклов даже после комментария, в котором я явным образом ссылаюсь на Nemerle?
НЛО прилетело и опубликовало эту надпись здесь

Обычно наоборот заменяют рекурсию на цикло ибо стаковерфлоу. И если вы не фанат передавать по 10 аргументов чтобы сделать TCO то лучше конечно так не делать.


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


Причем в нашем случае можно использовать "трюк с лямбдой" как выше подсказывают:


int n = 0;
|n| {
  while (n < 42) {
    n = process(n);
  }
}(n);

Только вот лучше я буду let писать чем вот это.

Потому что как различить a = 50 объявляет новую переменную или мутирует существующую? Рефакторинг превращается в боль:


b = 10;
if cond {
 a = 50
 print(a);
}
print(b);

Теперь переименование b в a внезапно ломающее изменение. Ну и так далее. Не, не надо такого счастья

НЛО прилетело и опубликовало эту надпись здесь

Ну это не сильвербуллет, но часть проблем снимает. А ещё если у нас есть let'ы то компилятор при рефакторинге может нам сказать что семантика принта может поменяться. В случае когда = может означать что угодно провести такой анализ куда сложнее

Компилятору для генерации кода всё равно приходится определять, какие из = — объявления, а какие — присваивания, так что при рефакторинге хоть с let, хоть без let он сможет сказать одно и то же.

Нет, компилятор когда дошел до = уже знает, присваивание это или биндинг, именно потому что они различаются с помощью let.

Ну тут выше был приведён контр пример
let x;
if cond1 {
  x=12;
} else {
  return;
};

может полный компилятор и знает разницу здесь, но вот какой нибудь кастрированный интелисенс/человек уже не может так однозначно сказать, что это здесь присваивание или все же хитромудровыебанная раздельная инициализация.

Просто присваивание. То что оно единственное и все такое — доп. контекст, который может вам и не нужен. Если он интересен можно тыкнуть в x и посмотреть что это.


Не говоря про то, что в реальности let x; я не встречал ну ни разу. Чаще будет что-то вроде:


let x = 
  if cond1 {
    12
  } 
  else {
    return;
  };

Если уж мы экономим горизонтальное место.

Нет, это не присваивание. Это часть раздельного объявления. Скажите мне пожалуйста какого типа переменная let x; и какое в ней значение ДО присваивания и не забудьте про адрес памяти где она лежит?

тип {unknown} (по аналогии с {integer} для констант), значения очевидно никакого нет. Место в памяти решается так же как и с {integer} — по первому использованию.

НЛО прилетело и опубликовало эту надпись здесь

Потому что в расте нет индирекции по-умолчанию. Нельзя навесить уравнение Monad например потому что нет такого типа. Есть либо Box либо SomeConreteStruct. Единственный сценарий который я могу придумать — ссылка &dyn Something на стеке, который ну ооочень редко встречается и рассматривать его можно разве что с эстетически-гипотетической точки зрения.

Поэтому даже списки в расте это не совсем монады, потому что вместо сигнатуры
(a -> f b) -> f a -> f b
у них сигнатура
(g : a -> f b) -> f a -> FlatMap (f a) g

НЛО прилетело и опубликовало эту надпись здесь
Мой комментарий может показаться вам слишком резким, и я не хочу обидеть лично вас, но то что вы написали — полная чушь.

Раст это статически типизированный ЯП, а это значит что присваивание НЕ меняет ТИП переменной, тогда как по вашей версии сначала тип unknown, а потом int или еще что то. Также в расте не существует типа unknown. Еще больше, присваивание семантически это всегда создание КОПИИ объекта (не важно глубокой или неглубокой), что бы создать копию, ваш левый операнд в операции присваивания ДОЛЖЕН быть валиден на момент копирования — какой инвариант имеет значение
значения очевидно никакого нет.
? Отсутствие значение это НЕ валидное значение это вообще НЕ значение. Этот как NAN не число или null не значение. Нельзя разыменовывать нулл поинтер. И естественно адреса у такой переменной НЕТ и быть не может, т.к. ни тип ни значение ещё не известны.

Я ещё не достаточно хорошо знаю раст поэтому расскажу на примере с++ — присваивание это такая специальная операция которую можно переопределить — предоставить свою реализацию. Смысл в том что операция может быть вызвана только лишь на уже сконструированном и валидном объекте, но let x; не вызывает НИКАКОГО конструктора не аллоцирует ресурсы и не создаёт валидную переменную, у которой можно было бы вызвать операцию присваивания далее по коду.

Еще разок, объявление это связывание уже сконструированного объекта с именем всегда гарантированно 0 рантайм оверхед.
Присваивание это создание КОПИИ объекта связанного с именем справа от знака равно используя ресурсы объекта связанного с именем слева от оператора =, это либо дефолтная реализация копирование содержимого памяти объекта справа в память объекта слева (предполагаем что они/адреса всегда разные) либо кастомная операция. Надеюсь я смог объяснить почему вы не правы.
Раст это статически типизированный ЯП, а это значит что присваивание НЕ меняет ТИП переменной, тогда как по вашей версии сначала тип unknown, а потом int или еще что то.

Почему-то это именно так и работает для текущих цифровых типов:


fn foo() {
    let x = 10;
    if x == "Hello" {

    }
}

error[E0277]: can't compare `{integer}` with `&str`
 --> src/lib.rs:3:10
  |
3 |     if x == "Hello" {
  |          ^^ no implementation for `{integer} == &str`
  |
  = help: the trait `PartialEq<&str>` is not implemented for `{integer}`

Что это за тип такой {integer}? Сколько байт в "статически компилируемом расте" он занимает, 8 или 16 или 32 или ещё сколько?


Еще больше, присваивание семантически это всегда создание КОПИИ объекта (не важно глубокой или неглубокой), что бы создать копию, ваш левый операнд в операции присваивания ДОЛЖЕН быть валиден на момент копирования — какой инвариант имеет значение

Конечно не означает, иначе не-копи типы нельзя было бы присваивать, но почему-то можно.


Я ещё не достаточно хорошо знаю раст поэтому расскажу на примере с++ — присваивание это такая специальная операция которую можно переопределить — предоставить свою реализацию.

А в расте не как в С++, в нем присваивание нельзя переопределить — и присваивание всегда является мувом значения из одного места в другое. В случае, если тип — копи, и его нельзя переместить, то вставляется неявное копирование, для удобства. В противном случае будет ошибка "тип нельзя переместить в переменную". А чтобы что-то переместить в переменную изначально там не обязательно что-то должно быть.


Присваивание это создание КОПИИ объекта связанного с именем справа от знака равно используя ресурсы объекта связанного с именем слева от оператора =, это либо дефолтная реализация копирование содержимого памяти объекта справа в память объекта слева (предполагаем что они/адреса всегда разные) либо кастомная операция. Надеюсь я смог объяснить почему вы не правы.

Если бы это был код на С++ со своими 100500 разных конструкторов, перегрузок базовейших операторов и т.п. то я наверное был бы неправ, только мы не про С++ говорим. Если что-то верно в С++ это не обязательно универсальная правда для всех языков, вот и все

Я ещё не достаточно хорошо знаю раст поэтому расскажу на примере с++ — присваивание это такая специальная операция которую можно переопределить — предоставить свою реализацию. Смысл в том что операция может быть вызвана только лишь на уже сконструированном и валидном объекте

Это именно что "плюсизм", который не имеет никакого отношения к Rust. В Rust о присваиваниях заботится компилятор, и программист не может этого переопределить. Можно только выбрать будет ли тип копируемым или перемещаемым. Как следствие — нет ограничений вроде "операция может быть вызвана только лишь на уже сконструированном и валидном объекте", компилятор знает что делать если "целевой объект" ещё не сконструирован и не валиден.

Здесь ответ обоим @mayorvp и PsyHaSTe на коменты выше

Да признаю, семантика = в расте не такая как в плюсах. Нет, вы все ещё не правы на счёт let; Вы нашли изъян в моих НЕСУЩЕСТВЕННЫХ тезисах. Вы все еще никак не отреагировали на суть:
  • Полностью выведенный тип переменной не может быть изменён операций присваивания
  • После полного определения любая переменная должна иметь конкретный ТИП, валидное ЗНАЧЕНИЕ и АДРЕС

Если вы все ещё прибываете в заблуждении насчёт что let x; это полное определение (а не часть раздельного), то попытайтесь ответить на такой вопрос — если бы следующий код был бы валидным (а он не компилируется без ошибок), то что по вашему мы бы увидели в консоли?
fn type_of<T>(_: &T) -> &'static str {
    std::any::type_name::<T>()
}

fn main() {
let x;// = 1;
println!("{}, {}, {:p}", type_of(&x), x, &x);
}


Что это за тип такой {integer}

Ответ смотри здесь. Я так понимаю что {integer} это ВНУТРЕННЕЕ обобщенное(не доконца выведенное) представление типа числовой переменной и является деталью реализации компилятора и НИКОГДА не доступно из программы. Иными словами это как конецпт интегрального типа в с++ — шаблонный тип с определенным интерефейсом. В валидной программе тип любой переменной ВСЕГДА будет выведен до самого конца и в случае числовых констант дефолтный выбор это i32.

Если в моем примере выше вы напишите let x = 1; то на консоль быдет распечатано: i32, 1, 0x7ffe490bef0c
Видите никаких {integer} здесь и не пахнет. i32 это валидный КОНКРЕТНЫЙ заместитель обобщеннго {integer} т.к. полностью удовлетворяет всем тербованиям {integer}.

И ещё раз для закрепления — ошибка компилятора о том что тип {intger} в очередной раз подтверждает мой тезис — в валидной программе все типы выведены до конца, неполное объявление, а затем попытка использования приводит к ошибке компилятора где он сообщает максимум известной информации на момент ошибки. Т.е. если удалость понять что это {integer} уже хорошо давайте скажем прогера хоть что то о типе это лучше чем ничего не сказать ни смотря на то что конкретный тип не был выведен.
Если вы все ещё прибываете в заблуждении насчёт что let x; это полное определение (а не часть раздельного), то попытайтесь ответить на такой вопрос — если бы следующий код был бы валидным (а он не компилируется без ошибок), то что по вашему мы бы увидели в консоли?

{unknown}


Ответ смотри здесь. Я так понимаю что {integer} это ВНУТРЕННЕЕ обобщенное(не доконца выведенное) представление типа числовой переменной и является деталью реализации компилятора и НИКОГДА не доступно из программы. Иными словами это как конецпт интегрального типа в с++ — шаблонный тип с определенным интерефейсом. В валидной программе тип любой переменной ВСЕГДА будет выведен до самого конца и в случае числовых констант дефолтный выбор это i32.

Ну вот и {unknown} такой же — ВНУТРЕННЕЕ обобщенное(не доконца выведенное) представление типа любой переменной. Которе является деталью реализации компилятора и НИКОГДА не доступно из программы (как кстати видно в примере выше "если бы это компилировалось"). Один в один.

Я не знаю, может у вас свой персональный компилятор раста, но тот который в песочнице говорит error[E0282]: type annotations needed
Я нигде не вижу никаких {unknown}.

Ну вот и {unknown} такой же — ВНУТРЕННЕЕ обобщенное(не доконца выведенное) представление типа любой переменной. Которе является деталью реализации компилятора и НИКОГДА не доступно из программы


Противоречит вашему предыдущему ответу, что на консоль в качестве имени типа будет распечатано {unkown}. Тут либо на консоль пойдёт что то другое либо оно доступно из программы.

А сам факт того что оно не компилируется + текст ошибки говорит о том, что определение переменной x неполное. Я до сих пор не пойму о чем тут можно спорить дальше.

В помощь вам простая модификация моего примера, сделайте то о чем просит компилятор — добавьте аннотацию типа например let x: i32; И тогда следующая ошибка будет borrow of possibly-uninitialized variable: `x`

Видите — тип уже есть (адрес думаю тоже), но вот валидного значения все ещё нет — незаконченное определение. Если вы пуститесь во все тяжкие используете unsafe, то тогда требование валидного значения снимается и там же в ансэйфе вам будет доступен адрес. Но даже для ансэйф нужен тип.

Да даже если мы представим, что ваш персональный компилятор в котором есть {unknown} существует, то пользы/смысла в этом нет. {integer} очень полезен т.к. имеет предопределённый интерфейс и компилятор даже не зная конечный тип уже может генерить дженерик код для такой переменной используя интерфейс {integer} т.к. любой конечный подходящий тип будет его реализовывать.

Я не могу представить интерфейс {unknown} — он пуст /бесполезен. Даже Void (тип с монозначением) более полезен чем ваш {unknown}. Ваш тип {unknown} не имеет значений — пустой. Из основ ФП я знаю, что нет ни одной функции из пустого типа во что бы то ни было поэтому он бесполезен на практике.
Из основ ФП я знаю, что нет ни одной функции из пустого типа во что бы то ни было

Вообще-то есть, функция идентичности.

НЛО прилетело и опубликовало эту надпись здесь
Да вы правы, ещё раз перечитал, я ещё довольно таки плаваю в основах ФП и могу нести не то что есть на самом деле, спасибо за поправку. Мой мохзг запомнил «главное» то что захотел, а не то что есть category-theory-for-programmers 2.6
You can define a function that takes
Void, but you can never call it. To call it, you would have to provide
a value of the type Void, and there just aren’t any.
Пардон, мой комментарий выше следует читать «Компилятору
языка без let для генерации кода всё равно приходится определять, какие из = — объявления, а какие — присваивания, так что при рефакторинге хоть на языке с let, хоть на языке без let компилятор сможет сказать одно и то же.» (Я выделил добавленные слова.)

Если мы про раздельное написание лета и присваивание то тут у нас нет никакого разночтения — это просто обычное присваивание, записывание в переменную или как угодно ещё. Компилятору все равно требуется проверить, что в переменную можно писать. А условия записи:
binding.ty == Modifiers.Mut || binding.assignment_count == 0
С точки зрения парсера есть только присваивание в данном случае. Энфорсингом единственной инициализации является уже семантическая модель раста, как и какой-нибудь борровчекер.

Ради интереса взял сишарп (просто потому что у него есть удобное компиляторное апи), посмотерть, как он различает "первоначальную инициализацию" и присваивание.


Что оказалось — а никак.


Код


int x;
x=0;
x=1;

Дерево:


img


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

Я думаю вы согласитесь что нет никакой разницы (для результата вызова pint(a)) объявление это или присваивание. Теперь что насчет кода который включает этот скоуп? Если это объявление то все хорошо — никаких проблем, а если это присваивание то это побочный эффект по отношению к локальному скоупу if

Я лично предпочел бы явное разделение побочного эффекта что то типа
a = 10
a = if cond {
   a = 50
   print(a) // нужно быть марсианином что бы ожидать что вывод будет 10
   a
} else { a }
print(a) // результат вывода зависит от cond точно так же как и в вашем оригинальном примере (если заменить b на а)


В этом случае мне не важно была а объявлена до этого момента и это присваивание или это объявление а т.к. любой код который использует а увидит то что я ожидаю — 50 в случае если cond истинна и 10 иначе. Вы даже можете убрать первичное объявление а и заменить его на 10 в else ветке и ничего не поменяется.

В вашем примере я не понимаю, a = 50 должно мутировать a в нижнем принте или нет. Является ли a = 50 ошибкой и должно быть b = 50 или нет? С летом вопрос однозначный, а в этом коде неоднозначность.


Второй вопрос a = 10 — это мутабельный биндинг или нет? Имеем ли мы вообще право его мутировать в первую очередь?
вопрос три — а нет ли ещё выше по скоупу (вне нашего примера) ещё какой-нибудь a? Должны ли мы мутировать эту внешнюю а или опять же мы должны её затенить? Вопрос, вопросы...




Странно вообще воспринимать придирки что целых 3 буквы требуют писать, а с другой стороны те же люди говорят что fn это слишком коротко.

я никогда не утверждал, что fn это плохо, по мне так лучше его вообще выпилить или заменить на хаскелевский синтаксис ->, но тут мне хотя бы понятна где возникает неоднозначность и как fn реально помогает её разрулить.

Да мне понятны ваши вопросы, вопросы… Это просто система привычек которые у нас сформировались — использование побочных эффектов.

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

let mut x = 10;
if cond { x = 5; } // это ЗЛО побочный эффект, т.к. x ГЛОБАЛЬНАЯ переменная для if скоупа
x = if cond { 5 } else { x }; // не зло т.к. ПЕРЕОПРЕДЕЛЕНИЕ (может быть с оптимизированно в присваивание). возможно else бранч не обязателен, не знаток раста


Мне НЕ нужно видеть что происходит внутри любого вложенного блока, я точно могу быть уверенным что внутри НИЧЕГО видимого здесь поменяться не может. Также я вижу что x переопределяется и что с этого момента он имеет другое значение.

Сейчас в расте да и в других языках мне НУЖНО заглянуть внутрь вложенного блока что бы узнать, а не мутирует ли он что либо — это и есть нарушение локальности.

В моей вселенной x = 5 ВСЕГДА локален тому скоупу в котором вы его видите. Т.е. не важно есть там наверху переменная с таким именем или нет ее, x = 5 только здесь и сейчас -до конца скоупа или до следующего ПЕРЕОПРЕДЕЛЕНИЯ x в том же скоупе. Скоуп закончился все нет никакого x. Если выше был объявлен другой x то он НИКАК не поменяется это совершенно ДРУГОЙ не связанный х.

Все бест практисы для всех языков советуют просто давать локальным переменным ДРУГИЕ имена, что бы они ничего не хайдили и тем самым добиваться того поведения о котором я пишу — НЕ МУТИРОВАТЬ переменные из вышестоящего скоупа до тех пор пока это не вредит производительности. Проблема в том что давать другие имена не всегда лучшее решение, часто бывает что имена локальных переменных становятся муторнее/менее понятными. Это простительно для языков спроектированных ДО раста.

если мне нужна мутабильность то я буду писать
mut x = Type{5};
что позволит мне вызывать мутирующие методы класса/структуры Type. для переменной x, без mut только лишь не мутирующие методы.

Присваивание это частный случай мутации который семантически заменяет все содержимое объекта тогда как мутирующий метод может менять только лишь часть объекта. Опять же мутирующие методы это узаконенная оптимизация, вы всегда можете выразить мутирующий метод через код который создаёт копию объекта с изменённым состоянием. Есть целая идиома имутабл классов в шарпах и как самый яркий пример это строки.

var str = "xxx";
str += "x";

+= выглядит как мутирующий оператор, но на самом деле он НЕ мутирующий но создает копию и переопределяет str.

Есть только одна проблема с этим подходом, разработчики C# не осилили сделать оптимизацию которая вместо переопределения в рантайме вызовет специфичный/оптимальный/эффективный код конкатенации. У них была проблема/причина компилятор НЕ знал сколько ссылок есть на этот объект и поэтому они НЕ смогли сделать такую оптимизацию. Раст ЗНАЕТ сколько у него ссылок на это значение и МОЖЕТ реализовать такую оптимизацию когда ссылка ровно одна.

Поэтому для раста это изъян в дизайне, он может (без потери производительности) и должен был бы запретить присваивание как таковое исключая целый ряд потенциальных ошибок как невыразимых.
Смотрите побочные эффекты на глобальных переменных уже вроде бы как все адекватные люди признали злом, видимо нужно еще ндцать лет что бы все те же адекватные люди поняли, что нет ни какой разницы между мутаций глобальной переменной из функции и мутации переменной из вышестоящего скоупа в ТОЙ же функции — точно такое же зло.

Да какое это зло. Вот возьмите хачкель (или идрис если хачкель кажется вам слишком старым и накопившим легаси), вот написал я acc' <- newSTRef acc и дальше где-то ниже в каком-то ифчике мутирую. Что, зло? Никто так не делает? Да, мутировать когда можно не мутировать и использовать "expression everything" плохо, только вот не всегда это возможно. Раст это низкоуровневый язык, поэтому там мутирование например позволяет избежать лишних аллокаций, потому что caller может всегда передать свой буфер который функция будет мутировать, вместо принятой в гц-языках практики что функция сама саллоцирует и вернет значение. Типичный пример — чтение с консоли, в расте в стд эта функция требует чтобы ей передали первым аргументом &mut String. Как вы тут будете без mut жить?


В моей вселенной x = 5 ВСЕГДА локален тому скоупу в котором вы его видите. Т.е. не важно есть там наверху переменная с таким именем или нет ее, x = 5 только здесь и сейчас -до конца скоупа или до следующего ПЕРЕОПРЕДЕЛЕНИЯ x в том же скоупе. Скоуп закончился все нет никакого x. Если выше был объявлен другой x то он НИКАК не поменяется это совершенно ДРУГОЙ не связанный х.

Вопрос, почему в дофига ФП хачкеле в котором кстати никаких мутаций через = не существует всё равно есть паттерн let ... in ...?


Все бест практисы для всех языков советуют просто давать локальным переменным ДРУГИЕ имена, что бы они ничего не хайдили и тем самым добиваться того поведения о котором я пишу — НЕ МУТИРОВАТЬ переменные из вышестоящего скоупа до тех пор пока это не вредит производительности.

А бест практисы раста где можно шедовить переменные мне нравятся куда больше. Просто потому что вместо того чтобы выдумывать по 3-4 бесполезных названий вида user_string, user_parsed, user_reference_count_wrapper я могу просто сделать user и все. Зачем все эти мусорные переменные "рыбу заворачивали" мне в коде и в автодополнении, и в watch? Они несут чисто техническую нагрузку и облегчение понимания кода только в том месте, где они объявляются. Ниже про них знать не нужно.


если мне нужна мутабильность то я буду писать
mut x = Type{5};
что позволит мне вызывать мутирующие методы класса/структуры Type. для переменной x, без mut только лишь не мутирующие методы.

И как мы отличим присваивание от мутирования в таком случае?


let mut x = 1;
let mut y = 2;
(x, y) = (3,4);
println!("{} {}", x, y); // 3 4

+= выглядит как мутирующий оператор, но на самом деле он НЕ мутирующий но создает копию и переопределяет str.

Что конечно же неприемлимо для низкоуровневого высокопроизводительного языка. Рассчитывать что придет магический компилятор и весь наш говнок… простите, "легкочитаемый" код соптимизирует немного наивно. В ГЦ языках другой трейдоф, и там например не надо волноваться, на стеке или в куче у нас объект — в расте перемещение объекта из кучи в стек или наоборот — ломающее изменение, которое видно в типах. Наверное приводить пример "а вот в сишарпе вот так" не стоит, ведь у него совсем другие цели и подходы.

Вопрос, почему в дофига ФП хачкеле в котором кстати никаких мутаций через = не существует всё равно есть паттерн let… in ...?


Я не настоящий сварщик фпшник, но сочувствующий. Мой мозг так же как и мозг многих других разработчиков сильно пострадал подвержен влиянию с++. Спросите лучше 0xd34df00d говорят он успешно прошёл курс ФП терапии и больше не использует некачественные препараты небезопасные языки.

А бест практисы раста где можно шедовить переменные мне нравятся куда больше.


По этому вопросу я вам не оппонент, мне тоже нравится шэдовинг куда больше чем извращения с именами переменных, но в кругу с++ это считается одним из смертных грехов.

На остальные тезисы попробую ответить скопом т.к. они связаны. Я тут подумал и мне кажется я начинаю понимать в чем сложность донести мою мысль до вас, не уверен что все так и есть но думаю как то так: ваша нейросеть сильно прикипела к ключевому слову let и/или аналогам в других языках. Когда я говорю что оно лишнее ваши нейроны кричат что я сошел сума и кто-то пытается вас лишить очень уютного/удобного ментального костыля. Ну и ладно давайте я уважу вашу нейросеть, да и к тому же это неважная деталь для той мысли что я пытаюсь объяснить. Я переформулирую мою мысль с равенством первоначальной до изоморфии.

Я заявляю, что если вы начиная с этой секунды везде и всегда (на расте конечно) где ставите знак равно будете ещё ставить let, то ваш код станет только лучше и вы ничего не потеряете, другими словами добровольно откажитесь от того 1% случав где вы использовали присваивание и вместо него используйте шэдовинг с явным возвратом значение из вложенного скоупа в том случае если мутировали не локальную переменную.
fn main() {
let x = 1;
let y = 2;
println!("{} {}", x, y);  //1, 2
let (x, y) = (3,4);
println!("{} {}", x, y);  //3, 4
}

тоже самое но со вложенным скоупом
fn main() {
let x = 1;
let y = 2;
println!("{} {}", x, y);

let (x, y) = if x != y
{
	let (x, y) = (x + 2, y + 2);
	(x, y)
} else { (x, y) };
println!("{} {}", x, y);
}


Специально проверил оба примера валидный раст код. Обратите внимание на то что отпала необходжимость в mut. Да mut все еще нужен для вызова мутирующих/инплэйс функций, но это опять же несущественные детали не влияющие на суть идеи. Шарпы и другие языки НЕ МОГУТ В ПРИНЦИПЕ реализовать эффективную оптимизацию и заменить переопределение на присваивание, но не потаму что (не)испоьзуется гц, а потаму что раст реализовал уникальную функциональность компилятор раст знает сколько ссылок на объект, это ключевая информация для реализации этой оптимизации. Смотрите одна ссылка значит мутабельный, много значит немутабельный и нельзя оптимизировать — придется делать копии.

И как мы отличим присваивание от мутирования в таком случае?

Я думаю примеры выше прекрасно демонстрируют, что во первых нет никакой разницы между присваиванием и переопределением, и что еще важнее нам обсалютно до лампочки что из них одно, а что другое, это просто детали оптимизации. Компилятор САМ разберется где можно эффективно заменить переопределение на присваивание, а где нет.

Очень хорошо, что половину пути вы уже прошли:
Да, мутировать когда можно не мутировать и использовать «expression everything» плохо


А теперь пожалуйста покажите пример
только вот не всегда это возможно.


когда я не смогу применить свою идею(везде где есть = добаить let) на расте.

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


Как я уже неоднократно писал, все эти мутации это просто узаконенные оптимизации. Еще раз до эпохи раста это было РАЗУМНО, т.к. не было принципиальной возможности реализовать эти оптимизации на уровне компилятора, не было ключевой информации сколько ссулок на объект. Раст может сделать эту оптимизацию на уровне компилятора и поэтому наличие присваивания (и mut тоже) является ИЗЪЯНОМ дизайна раст. Да я понимаю что разработчики скорее всего не могли этого предвидеть заранее когда делали подсчет ссылок, но никогда не поздно что то улучшить, хотя конечно это то еще ломающее изменение языка и по сути что то типа раст2 или вообще другой язык. Хотя технически вы уже сегодня можете отказаться от многих mut и от всех присваиваний.

Ну а теперь как вишинка на торте, если везде и всегда писать let x = value; и нигде не иметь x = value; то очевидно что нет необходимости в let и можно просто везде писать x = value;

для полноты я проверил еще и мувыбл тип
fn main() {
let x = "x";
let y = "y";
println!("{} {}", x, y);

let (x, y) = if x != y
{
	let (x, y) = (x.to_owned() + "x", y.to_owned() + "y");
	(x, y)
} else { (x.to_owned(), y.to_owned()) };
println!("{} {}", x, y);
}

Как вот такое будет выглядеть без присваиваний?


const N:usize=10;
let mut a[N] = [1; N];
for i in 0..100 {
  a[a[i%N]%N] += 1;
}
Сначало, я думал что не смогу это написать, но потом немного подумав и немного погуглив выяснил, что раст движется в правильном направлении и все ужо хотова — правда в найтли.

// требуется nightly channel
#![feature(array_map)]

fn main() {
const N:usize=10;
let a = [1; N];
println!("{:?}", a);
let a = a.map(|x| x+1);
println!("{:?}", a);
}

[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
[2, 2, 2, 2, 2, 2, 2, 2, 2, 2]

Нет, этот код делает другое. Результат должен быть [3, 28, 20, 23, 6, 7, 7, 4, 5, 7].


Playground

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

fn main() {
const N:usize=10;
let a = [1; N];
println!("{:?}", a);

// если разкоментить то раст говорит что trait `FromIterator<usize>` is not implemented for `[usize; 10]`
//let a = (0..100).fold(a, |a, i| a.iter().enumerate().map(
let a = (0..100).fold(a.to_vec(), |a, i| a.iter().enumerate().map(
	|(j, &x)| if a[i%N]%N != j { x } else { x + 1 }).collect()
);
println!("{:?}", a);
}

песочница

Да это опять не 100% то что вы хотите (тип а изменился со статического масива на вектор), но это уже детали (arraymap есть в найтли значит нет фундаментальных проблем реализовать этот трайт для array). В любом случае вы просто заставили меня по упражняться в рэнджи, спасибо было интересно посмотреть как оно в расте — неплохо, мне понравилось.

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

А терь вы выделили х2 памяти на ту же операцию. Если например мы уже заняли всю память то ваш код вызывает OOM и падение приложения. Отлично.


Ну а по существу такую билиберду конечно никогда в реальном коде не увидешь

Ну это же сишная мантра "НАСТОЯЩИЙ ПРОГРАММИСТ НИКОГДА ТАКОГО НЕ НАПИШЕТ", не надо её тут нам. Пишут и не только так.


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


#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub struct TimeSpan(pub Duration);

const SECONDS_IN_MINUTE: i64 = 60;
const SECONDS_IN_HOUR: i64 = 60 * SECONDS_IN_MINUTE;
const SECONDS_IN_DAY: i64 =  24 * SECONDS_IN_HOUR;
const NANOS_IN_SECOND: i64 = 1_000_000_000;

impl From<&TimeSpan> for String {
    fn from(timespan: &TimeSpan) -> Self {
        use std::fmt::Write;

        let mut result = String::with_capacity(25); // max possible timespan length is 25 chars
        let duration = timespan.0;
        if duration.num_days() != 0 {
            write!(result, "{}.", duration.num_days()).unwrap();
        }
        let secs = duration.num_seconds();
        let nanos = duration.num_nanoseconds().unwrap_or_default() % NANOS_IN_SECOND;
        write!(
            result,
            "{:02}:{:02}:{:02}",
            secs % SECONDS_IN_DAY / SECONDS_IN_HOUR,
            secs % SECONDS_IN_HOUR / SECONDS_IN_MINUTE,
            secs % SECONDS_IN_MINUTE
        )
        .unwrap();
        if nanos != 0 {
            write!(result, ".{}", nanos).unwrap();
            result = result.trim_end_matches('0').into();
        }
        result
    }
}
result = result.trim_end_matches('0').into();

Ай :( Надо было что-то вроде


result.truncate(result.trim_end_matches('0'));

, чтобы новой аллокации не было

Переписал как


result.truncate(result.trim_end_matches('0').len());

спасибо! Вроде и длина и truncate ожидает длину в байтах, так что должно работать корректно.

Я рад что ваша риторика изменилась, с «без let ну вообще жизни нет никак» на «дайте мне тоже самое, но с перламутровыми пуговицами»
Покажите, как можно переписать этот код без mut и дополнительных аллокаций


Я изначально сказал, что на КОНЦЕПТУАЛЬНОМ уровне присваивание и (пере)определение это одно и тоже, но присваивание это узаконенная форма оптимизации (которая очень важна). Так же я говорил, что только лишь раст имеет возможность переложить эту оптимизацию с человека на компилятор, а другие языке не могут в принципе, но я нигде не говорил что раст УЖЕ реализовал все необходимые оптимизации. Это раз.

Зная основы ФП, даже не глядя на ваш код я уверен, что он может быть переписан без единого присваивания на расте, но вот ваше требование отсутствия дополнительных аллокаций (за базу для сравнения очевидно берем вашу реализацию) обоснованно примерно чуть более чем никак. Это два.

Ну и самое главное, каникулы закончились времени на дискуссии/упражнения в раст у меня больше нет, так что вам для саморазвития будет полезно самому попытаться переписать ваш собственный пример, для начало запилите все присваивания, а уже потом смотрите сколько там аллокаций может раст уже все сам разрулил, такое часто бывает, иногда бывает даже так что ФП подход приводит даже к БОЛЕЕ эффективному коду чем классика. Я вполне допускаю, что без доп аллокаций сегодня это переписать невозможно, но вы не отчаивайтесь, с каждым релизом раста эффективность будет расти.

«НАСТОЯЩИЙ ПРОГРАММИСТ НИКОГДА ТАКОГО НЕ НАПИШЕТ»
я такого не писал, я сказал что пример выше бессмысленная белиберда, полезная разве что только в качестве упражнения в ренджи, и поэтому насколько (не)эффективный код раст сгенерирует для этого примера меня лично вообще не интересует. Ваш пример куда разумней, но времени нет.
Я и в седьмой раз вам напишу, что «оптимизация аллокаций» — второстепенная польза от присваиваний, по сравнению с возможностью использования в программе циклов.
Если нет мутации внешних скопов, то всё, что происходит внутри цикла, остаётся внутри цикла — каждая итерация будет выполняться с теми же данными, как и первая.
Ещё раз, другими словами — ЛЮБОЙ цикл заменяется на рекурсию. ЛЮБАЯ рекурсия заменяется на цикл, хвостовая особый случай и заменяется эффективно. Если вы с этим не согласны, мне больше нечего вам сказать — учите мат. часть. Если вы с этим согласны, тогда вспоминаем мой первоначальный тезис — на КОНЦЕПТУАЛЬНОМ уровне (т.е. игнорируя детали реализации в том числе производительность) присваивание и определение это одно и то же.

Ещё раз, другими словами — структурное и функциональное программирование это разные КОНЦЕПЦИИ, и не следует подменять одну из них другой.

Не вы ли пытаетесь подменить программирование на Rust функциональным, завлекая не соответствующим действительности лозунгом «достаточно всюду писать let и явно вытаскивать изнутри if изменения внешних переменных, максимум что вы потеряете — оптимизацию аллокаций»?

Вы это точно мне пишете?

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

Любой код на С++ заменяется на ассемблер, любой ассемблер можно описать на С++. Если вы с этим не согласны, мне больше нечего вам сказать — учите мат. часть. Если вы с этим согласны, тогда вспоминаем мой первоначальный тезис — на КОНЦЕПТУАЛЬНОМ уровне (т.е. игнорируя детали реализации в том числе производительность) С++ и ассемблер это одно и то же.

Если нет мутации внешних скопов, то всё, что происходит внутри цикла, остаётся внутри цикла — каждая итерация будет выполняться с теми же данными, как и первая.


Дальше идет псевдо ассемблер:
push state //сохраняем первоначальное состояние цикла в стеке
push condition // сохраяняем счестчик/условие
loop: pop condition
cmp condition 0
jne break
call body
dec condition
push condition
jmp loop
break: ...

body: pop state
work with state localy
push new state
ret


Смотрите здесь почтивсе так же как просиходит в любом языке. Разница в том, что любой язык добавит непосредсвтенно перед безусловным jmp что то типа
pop state
Это будет соответсовать drop, выходу из скоопа/окончанию времени жизни возвращаемого/локального значения из тела цикла. НЕТ никаких технических проблем не делать этот pop state. Да синтаксиса это выразить нет, ну так это не моя беда.
Самое близкое что есть в расте это
let x = loop{
break value;
}

Смотрите опять же циклы в расте НЕ выражения — т.е. что бы что то вернуть из цикла нужно вызвать break со значением. То что я написал на псевдо асме делает цикл выражением таким же как if, но возвращая значение на каждой итерации. Последнее значение может быть присвоенно внешней переменной, а промежуточные значения переходят на следующую итерации. Т.е еще раз это механика фолд через цикл + один уровень стека. Вобщем то он так и реализован в любом языке, только через явное присваивание state на каждой итерации. Таким образом хвоствая рекурсия которая нуждается только в одно уровне стека и никогда не приведет к SO, сама функция не вызавает сама себя, а возвращает результат для следующего вызова на стеке и уже внешний цикл делает этот вызов. Да только хвостовая рекурсия может быть так записана (т.к. в общем случае потребуется более одного уровня стека), но так это именно то во что конвертируются циклы. В любом случае я показал на этом примере как именно низкоуровневая механика ЦИКЛА, а не механика рекурсивного вызова прекрасно сочетается с переопределением вместо присваивания.

Я бы предложил например такой синтаксис:
let x = 0;
let x = for let sum = x, value in (0..10) {
    let sum = sum + value // последняя итерация вернет значени на верхний скоуп, промежуточные итерации переопредялют sum для следующей итерации
}
println!({}, x); // должно печатать 55

а без let выглядит еще проще
result = for sum = 0, value in (0..10) {
    sum = sum + value
}

Думаю можно и получше синтаксис придумать, это не суть. Суть в том, что любой цикл я вам через fold выражу, а этот фолд потом в цикл и преобразую. Синтаксис при этом будет как у родного ванильного цикла. К if выражениям уже все привыкли и оценили их по достоинству, почему циклы не сделали выражениеми — загадка.
Проблема с размером аккумулятора для фолда.
Если мы не разрешаем менять внешний (для цикла) контекст, то в аккумуляторе должен быть вся изменяющаяся часть контекст (и возникает проблема с аллокацией, о которой уже упомянали).
А если разрешаем мутации внешнего контекста — то уже не возможна замена на фолд.
Мы все здесь ходим по кругу, по сути, вы от меня требуете предоставить вам работающий компилятор, да ещё и оптимизирующий, что бы алокации на присваивания заменял.

НЕТ его у меня и я пока не знаю/представляю как его сделать.

Все началось с того что без let ну никак нельзя, потом, сошлись что в принципе можно, но дескать надо все переписать на ФП, т.к. видите ли механика цикла не совместима с этим. Теперь уже и циклы не беда, только вот лядские алокации повылезали.

Я не думал глубоко над тем как оптимизировать переопределение и заменить его на присваивание, это не значит, что это уже кем то не придумано. Я почти уверен, что я вообще ничего нового здесь не придумал и уже есть десятки работ по CS где это уже лет как 30 все разобрано до мелочей в том числе как это оптимизировать, просто как это обычно и бывает ещё не пришёл тот человек/команда который все этого воедино соберёт.

Конечно я всего лишь анонимус из интернета, может я вообще gpt-3 бот, и верить на слово мне нельзя. Если вы убеждены, что такая оптимизация не возможна или представляет собой открытую проблему, то я бы послушал ваши аргументы почему вы так думаете — мне реально интересно.

Вот смотрите моё предсказание направления эволюции раста или его наследника — циклы станут выражениями, как я их здесь описал, присваивание уедет в ансайф (для тех случаев где с автоматической оптимизацией будет туго) со своим спец синтаксисом наподобие что был в паскале :=, let и mut исчезнет. За счёт этого аннотация лайфтайма сильно упроститься будет преимущественно автоматической примерно так же как сейчас type inference. Код будет выглядеть как будто написан на динамическом языке (в примере выше как раз демонстрируется смена типа переменной arr cо статического масива на вектор, но это уже и так доступно), но при этом будет полностью статически типизированным как сейчас.

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

Вот кстати меня это немного обескураживает, если я дичь говорю так перестаньте мне отвечать и/или заодно карму за минусуйте — это логично, дискуссия увянет сама собой и рот мне прикроет заодно. А тут так получается, одна рука (человек) рот затыкает, а другая (может даже те же самые люди, а может и другие) вступают в диалог и ждут ответа. Вот это вообще смысла не имеет. Не переживайте это не лично к вам, это так дань традиции высказать своё фу на систему кармы.
Я изначально сказал, что на КОНЦЕПТУАЛЬНОМ уровне присваивание и (пере)определение это одно и тоже, но присваивание это узаконенная форма оптимизации (которая очень важна).

А ещё на концептуальном уровне и быстрая сортировка и пузырьком — одно и то же, веть в итоге коллекция оказывается отсортирована! Или вот например — цикл и рекурсия одно и то же, зачем в компиляторах TCO завезли? Переписывают пустое в порожнее на ровном месте, нет чтобы оставить эквивалентные вещи в покое.


но вот ваше требование отсутствия дополнительных аллокаций (за базу для сравнения очевидно берем вашу реализацию) обоснованно примерно чуть более чем никак. Это два.

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

Я заявляю, что если вы начиная с этой секунды везде и всегда (на расте конечно) где ставите знак равно будете ещё ставить let, то ваш код станет только лучше и вы ничего не потеряете, другими словами добровольно откажитесь от того 1% случав где вы использовали присваивание и вместо него используйте шэдовинг с явным возвратом значение из вложенного скоупа в том случае если мутировали не локальную переменную.

Вы намеренно в пятый раз игнорируете циклы?

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

Невозможное возможно, если немного почитать основы ФП, я там ничего нового не изобрёл. Как видите никаких рекурсий, хотя наложение ограничения на рекурсию высосано из пальца ничем не обоснованно.
Ну так я в шестой раз обращаю ваше внимание на то, что там не только «где вы использовали присваивание и вместо него используйте шэдовинг с явным возвратом значение из вложенного скоупа в том случае если мутировали не локальную переменную», но и перекраивание структуры программы с отказом от цикла.

Можно вспомнить структурную теорему бома-якопини, где любую программу можно переписать с помощью одного гигантского свитча (для стейтмашины), циклом и ифчиками. Вопрос только, насколько это "теоретически верно" полезно на практике. Анекдот напоминает:


Дано: комната, где горит огонь, угрожающий превратиться в пожар, и рядом лежит куча песка.
Инженер: входит в комнату, засыпает огонь песком, уходит.
Физик: входит в комнату, насыпает песок вокруг огня, садится и
наблюдает за процессом.
Математик: входит в комнату, видит, что решение есть, и уходит.
Уточню, что даже без ифчиков: достаточно одного свича и одного цикла. Movfuscator не даст соврать %)
Вот я тоже не понимаю почему люди нуждаются в разделении объявления и присваивания

Если я опечатался в имени переменной, я хочу, чтобы это было ошибкой компиляции, а не тихим объявлением новой переменной.

покажите пожалуйста пример где компилятор(во вселенной где нет разницы в синтаксисе присваивания и объявления) не сможет распознать этой ошибки. Ну т.е. просто двух строчек не достаточно, нужно что бы из примера было явно видно как ожидаемое поведение расходится с реальностью и НЕ является артефактом использования побочных эффектов. за разъяснениями что я имею ввиду смотри habr.com/ru/post/532660/?reply_to=22488148#comment_22488470
Выше по-моему реалистичный пример.

вы главную претензию к эстетике Rust не объяснили.


Почему можно вот так вот:


def foo(a, b):
    print(f"{=a}, {=b}")

но


fn foo<T: Debug, H: Debug>(a: T, b:H){
    println!("a: {:?}, b: {:?}", a, b);
}

Упор на синтаксис println!. Им религия запретила завести f-expressions в макрос стандартной библиотеки?

Можете развернуть свой вопрос?

Почему в rust не используют более краткие и человеколюбивые f-expressions (когда имена переменных in-line со сторокой форматирования) и используют С-подобный выкрутас, когда параметры в интерполируемой строке и значения параметров передаются раздельно?

НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь

Окей. В стандартной библиотеке есть и такое, читайте модуль std::fmt.

Вы меня заставили ещё раз перечитать. Я так и не нашёл как можно захватывать перменные по имени. Я вижу, что можно по имени печатать аргументы, но это совсем не то же самое, что печатать переменные.

Просто решили что в стандартной библиотеке это не нужно. Если есть желание, качайте любой из 100500 крейтов поддерживающих интерполяцию, например https://docs.rs/fstrings/0.2.3/fstrings/

Просто решили сделать неудобно. Просто ответ на вопрос который просто ничего не объясняет.

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

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

Не, вот конкретно format_args!, на который println! в конечном счёте опирается, захардкожен.

Это уже да, но формат аргс который бы захватывал окружение был бы слишком магическим

Я не пробовал писать макросы раста, но я не вижу особой проблемы трансформации одного в другое. Главное же достоинство f-string в том, что можно захватывать имена переменных, что делает отладку кратно удобнее.

Похоже, вы пропустили добавление макроса dbg!. Он не решит вашу проблему с отладкой?

ого, спасибо, не знал. Интересно.

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

на самом деле это тривиальная трансформация: f"{varname}" => "{}", varname.

Для отладки в расте есть `dbg!` (что для отладки куда удобнее питоновских f-strings).

Наверное решили, что незачем предлагать кучу вариантов сделать одно и то же в стандартной библиотеке, достаточно базового. А базовый это строки отдельно, параметры отдельно, потому что это позволяет компоновать макросы:


println!("a: {:?}, b: {:?}", my_macro!(a), my_another_macro!(b));

Если вы начнете засовывать подобные компоновки внутрь форматной строки, то


  • форматная строка может стать очень длинной и потерять в лаконичности
  • ее сложнее локализовывать

Насколько я вижу, существует RFC, оно даже принято.

Жду с нетерпением!

Отличная статья, спасибо.


Насчет претензий к синтаксису раста всегда предлагаю альтернативу: как по-вашему стоит изменить синтаксис чтобы стало лучше? И вот на мой вкус ни одна предложенная альтернатива не лучше, а многие просто хуже, особенно в части лайфтаймов. И только некоторые предлагают менять шило на мыло, например угловые скобки на квадратные. Но тут уже вопрос "из двух равноценных альтернатив команда языка должна была выбрать какой-то один". Тем более что угловые используются чаще: Java/C#/Typescript/C++/...


Что до лямбд: то текущий синтаксис позволяет например деконструкцию в аргументах


let pairs = [(1,2),(3,4)];
let foos = pairs.map(|(x,y)| x + y);

Другие языки, например сишарп, на таком примере буксуют и не могут отличить деконструкцию пары от лямбды с двумя аргументами. И приходится писать уродцев в стиле


var foos = pairs.Select(pair => pair.Item1 + pair.Item2);

или


var foos = pairs.Select(pair => 
{
    var (x, y) = pair;
    return x + y;
});
В Eiffel, Python и Scala квадратные, в D и VB.NET круглые.
Что до лямбд: то текущий синтаксис позволяет например деконструкцию в аргументах
let foos = pairs.map(|(x,y)| x + y);

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

Python2 тоже умел, но в pep-3113 распаковку аргументов выпилили, так что Python3 так не умеет.

Причём выпилили под совершенно, на мой взгляд, притянутым за уши предлогом.

То, что он выглядит притянутым за уши в 2021, не значит, что он выглядел притянутым за уши в 2007.
Или о том, как я наконец-то решил поучиться Rust — его синтаксис очень уж нравится)
Имхо, вопрос о синтаксисе слишком субъективно индивидуален. Например, я психологически не переношу знак доллара перед переменными и плитки в винде. Логически я могу понять плюсы плиток, но… по факту у меня глаза становятся врастопырку, всё ералашит и ничего не могу с этим поделать. Смотрю на плиточный интерфейс и ничего не вижу. А кто-то плитками нормально пользуется. А вот к фигурным скобкам я почему то отношусь спокойно. Так что я думаю, проблема неприятия некоторых синтаксисов и интерфейсов индивидуальна и лежит в психологической плоскости.

Что касается вышеприведенных примеров, то некоторые вещи мне явно не по нраву. Например, разное поведение вызова функции с точкой с запятой на конце и без таковой. Как я понял, без точки с запятой подразумевается неявный return. Правильно? Но здесь нарушается принцип: явное лучше неявного. Да и точку с запятой легко пропустить/не заметить.

Точка с запятой влияет не на return, а на результат: без неё что вычислили — то и получилось, с ней получается всегда ()

Надеюсь, для людей, рекомендующих «правило спирали» в аду есть отдельные котлы. Помню, на stackoverflow тоже все рекомендовали, не разобравшись. За десять лет, наверно, уже можно было понять, что это правило неправильное, нет? Как оно разберет эту строку, например?
int M[3][3]

А никак, потому что это бред.
Вот есть нормальное правило «право-лево»: cseweb.ucsd.edu/~ricko/rt_lt.rule.html
1. Сначала разбираем всё справа.
2. Потом разбираем всё слева.
3. Если есть круглые скобки, то порядок меняется, как в арифметических выражениях.
Всё.

Заметно, что авторы языка Раст вдумчиво изучали синтаксис разных языков. В отличие от хейтеров синтаксиса Раст, которые кроме С-подобного синтаксиса никогда ничего не видали, и объявление типа не в том месте вызывает у них когнитивный диссонанс.
Когнитивный диссонанс вызывает то, что в английском (как и в русском) определение идёт перед определяемым словом, поэтому «int n» читается как текст на родном языке, а «n: int» — как будто по-албански.
int * a, b;

b — указатель или просто целое? А если так:


int a[], b;

?

Уже в Java этих заморочек нет: int[] a, b; трактуется вполне однозначно.

Если читать ':' как "which is", то и запись let x: int = 1 вполне нормально читается "let [be] an x which is an integer of value 1". В обычном языке смысл двоеточия тот же самый: он ставится перед определением, описанием, детальным объяснением.

А зачем? Кобол уже есть.

К слову можно вспомнить воспоминания лида команды C#, который пожалел, что в сишарпе выбран "сишный" синтаксис "сначала тип, потом имя" (пятый пункт сожалений).

в тайпскрипте Андрес Хейлсберг решил не повторять своей предыдущей ошибки и исправился

Да. Тенденции современных языков как раз показывают, что var: type это самый естественный для восприятия способ записи.

Да ладно. В толковых словарях наоборот. Полагаю, что в любых языках

Как минимум, в Оксфордском словаре я вижу «Red Army», «Red Cross», «red giant», «Red Sea» именно в таком написании:
archive.org/details/oxfordenglishref0002unse/page/1208/mode/2up

Можете привести пример толкового словаря, где бы было наоборот? На любом языке.

А где в названии "Red Army" определение, и где определяемое слово?

Армия, которая когда-то была названа красной, ...

Как вам уже намекнули, вы привели только определяемое слово, а самого определения нет. Я же не зря сказал толковый.


А вот нежирный текст по вашей ссылке — как раз определение, и оно после определяемого слова.

Как я уже намекнул, вам бы для начала вспомнить из школьной программы, что такое определение и что такое определяемое слово. Ссылку на википедию я дал.

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

Тогда покажите хоть один источник, где бы понятия «определение» и «определяемое слово» использовались не со школьными значениями, а с вашими.
Намекну, что нежирный текст в толковом словаре называется толкованиями — оттого и название словаря.

А можно перестать докапываться "До фонаря" и понять, что имелось в виду под токсичностью — в частности, это можно читать как "Недружелюбность", имея в виду что недружелюбный синтаксис — это вполне понятный для всех присутствующих термин. Если у вас проблемы с тем, что слово "токсичный" стало очень популярным в этих ваших интернетах и его используют направо и налево — ну, держите наверное свои чувства при себе, срач на сотню комментов ради этого разводить наверное не стоит.


А уж просить "определение определения" это уже куда-то совсем глубоко в демагогию, некрасиво так делать.

Я где-то выражал недовольство словом «токсичный» или просил «определение определения»?

Использовать термины из школьной программы, придавая им собственное, никем другим не используемое значение — вот как некрасиво делать.
Ниже я ответил по существу: законы естественного языка и законы формального — различаются. Точно так же в естественном языке мы скажем Московский авиационный институт ордена Ленина и октябрьской Революции имени Серго Орджоникидзе, а на формальном — Московский ордена Ленина и октябрьской Революции имени Серго Орджоникидзе авиационный институт. Хотя казалось бы

n: int совершенно естественно читается как n of type int (во всяком случае, у меня).

В быту вы тоже говорите «пиво производства Бельгии» вместо «бельгийское пиво», и «двигатель системы Дизеля» вместо «дизельный двигатель»?

Во-первых, аналогия, на мой взгляд, не совсем корректная. Пусть "пиво производства Бельгии" я не говорю, но с чтением строки "Пиво (Бельгия)" в каком-нибудь интернет-магазине у меня проблем не возникнет. Никто не читает по словам, всегда сканируется больший объем информации (например, предложение/строка).


Во-вторых, n of type int звучит вполне себе нормально, как уточнение типа переменной, если это важно в контексте. Особенно в языках с выведением типа, где тип переменной может опускаться. В этом случае даже проще бегло читать, потому что знаешь, что название переменной будет всегда, и оно всегда будет на первом месте, а вот тип – это дополнительная информация и может опускаться.


В-третьих, это довольно популярная практика, это же не Rust решил сделать "не как у всех": Typescript, Kotlin, Scala, Python, Swift, Go.

Обычно говорят просто "Пиво", а потом если клиент приподнимает брови уточняют "Бельгийское, Светлое". Вот так и в ЯПе пишут


let beer = 10;

А если непонятно, то уточняют


let beer: Belgian<Light> = 10;
Синтаксис моей мечты:
val x = 3.14
int val y = 42
string var z

Машиной парсится без проблем, человеком читается как проза, но нигде так делать не стали : С
Мне не нравится. Как раз потому что получается лесенка из имен. с летми я могу с расстояние в 2 метра увидеть все места где происходит инициализация, потому что они задают видный паттерн.

Ну и плюс я процитирую уже линкуемую статью Липперта:

As noted in item #6, C# borrows the «type first» pattern from C and many of its other successor languages:

int x;
double M(string y) { ... }


Compare that to Visual Basic:

Dim x As Integer
Function M(Y As String) As Double


or TypeScript:

var x : number;
function m(y : string) : number


Okay, dim is a little weird in VB, but these and many more languages follow the very sensible pattern of «kind, name, type»: What kind of thing is it? (A variable.) What is the variable's name? («x») What is the type of the variable? (A number.)

By contrast, languages such as C, C#, and Java infer the kind of the thing from context, and consistently put the type before the name, as if the type is the most important thing.

Why is one better than the other? Think about how a lambda looks:

x => f(x)


What is the return type? The type of the thing to the right of the arrow. So, if we wrote this as a normal method, why would we put the return type as far to the left as possible? From both programming and mathematics, we have the convention that the result of the computation is notated to the right, so it's weird that in C-like languages the type is on the left.

Another nice property of the «kind, name, type» syntax is that it's easy for the beginner programmer, who can see right there in the source code that «this is a function, this is a variable, this is an event,» and so on.

The lesson: When you're designing a new language, don't slavishly follow the bizarre conventions of predecessor languages. C# could have put type annotations to the right while still being entirely comprehensible to developers coming from a C background. Languages like TypeScript, Scala, and many more did just that.


Обратите внимание на «bizarre conventions of predecessor languages». Это оно и есть. Ваш вариант думаю получше, чем то что щас принято в С++/..., но варианту с летом он уступает. И это при том что я сам с сишарпом то уже почти 10 лет знаком, и синдром утенка по идее должен быть силен. Но оказывается, что леты очень удобны. А синтаксис type first, name later — не очень. Особенно когда у вас в реальном коде возникают вещи вроде:

Result<serde_json::Value, Vec<String>> val result = do_result();
val x = 3.14
&CustomSettingsMerger val custom_settigns_provider = extensions.get();
...


Кмк куда аккуратнее записывать:

let result: Result<serde_json::Value, Vec<String>> = do_result();
let x = 3.14;
let custom_settigns_provider: &CustomSettingsMerger = extensions.get();


Ну и все же я почти уверен что ваш вариант парсить сложнее, потому что с &CustomSettingsMerger непонятно, это мы берем ссылку на переменную с именем CustomSettingsMerger (и дальше будет например присваивание) или это имя типа Ref&ltCustomSettingsMerger> (что имеет место на самом деле в нашем случае)?
Why is one better than the other? Think about how a lambda looks: What is the return type? The type of the thing to the right of the arrow. So, if we wrote this as a normal method, why would we put the return type as far to the left as possible?

С функциями не всё так однозначно: по-русски «строковая функция» может означать как возвращающую строку, так и принимающую строку, и запись на ЯП, аналогичная «функция, возвращающая строку», безусловно понятнее, чем в сишном порядке. Есть и третий вариант: fn foo: int (string x, char y) — с типом результата после имени функции, но перед параметрами — с дополнительным преимуществом, что обозначение типа функции сгруппировано компактно, без имени посередине, как в C++.
Но претензия Липперта к тому, что синтаксис лямбды резко отличается от синтаксиса обычной функции, применима и к Rust.

Another nice property of the «kind, name, type» syntax is that it's easy for the beginner programmer, who can see right there in the source code that «this is a function, this is a variable, this is an event,» and so on.

В моём варианте (с явными словами val и var) такой проблемы нет.

Ну и все же я почти уверен что ваш вариант парсить сложнее, потому что с &CustomSettingsMerger непонятно

Не вижу проблемы: syntax tree в обоих случаях одинаковый.
С функциями не всё так однозначно: по-русски «строковая функция» может означать как возвращающую строку, так и принимающую строку, и запись на ЯП

Но ЯПы все же строятся на англоязычной грамматике, поэтому подходит с двусмысленностями русского языка к этому как-то странно


Но претензия Липперта к тому, что синтаксис лямбды резко отличается от синтаксиса обычной функции, применима и к Rust.

Как раз в расте лямбда описывается как let x: fn(i32) -> i32, а функция описываестся как fn x(i32) -> i32. Как по мне, один в один, разве что имя функции в одном случае встречается в середине, а в другом случае в начале. Я бы предпочел в ML стиле чтобы всегда объявлялось как лямбда (как кстати в JS щас модно делать), но в целом максимально близко в терминах сишного синтаксиса.


В моём варианте (с явными словами val и var) такой проблемы нет.

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


Не вижу проблемы: syntax tree в обоих случаях одинаковый.

В одном случае это
Reference { lifetime: ImplicitLifetime, value: Expression(Identifier("CustomSettingsMerger")))
а в другом
Type(ReferenceType { lifetime: ImplicitLifetime, target: Identifier("CustomSettingsMerger") }) }


Что-то не очень одинаковый.

Но ЯПы все же строятся на англоязычной грамматике, поэтому подходит с двусмысленностями русского языка к этому как-то странно

В этом конкретном аспекте между ними разницы нет.

Как раз в расте лямбда описывается как let x: fn(i32) -> i32

Я про окружение параметров символами, которые во всех остальных контекстах означают «или».

Во-первых val и var в целом похожи до смешания, разглядеть в коде разницу очень сложно.

В Kotlin и Scala — val и var, в C# и JavaScript — const и var; мне оба варианта одинаково понятны.

тип у них один и тот же — «переменная»

Для функций я в том же комментарии предложил fn. В любом из случаев, в определении явно прописано, переменная это, функция или что-то ещё, — в отличие от C++/Java.

Что-то не очень одинаковый.

Если и так, то один в другой тривиально конвертируется, как только парсер доходит до var; не нужен backtracking и повторный разбор. Ничего похожего на a*b; в С++, которое то ли умножение, то ли объявление указателя — на этапе парсинга определить невозможно.
НЛО прилетело и опубликовало эту надпись здесь
Мне нравится, как в VB.NET сделали List(of x): опять же, читается как проза.
НЛО прилетело и опубликовало эту надпись здесь

"Lenses give me a s t a b to my eyes"

НЛО прилетело и опубликовало эту надпись здесь
В естественных языках не бывает глубоко вложенных сложных определений. Поэтому они никак не могут тут служить образцом.

А сишная манера даже при небольшой сложности практически нечитаема, да еще и парсинг контекстно-зависимый.
«Тип перед названием» не обязательно означает «сишный синтаксис».
Уже в Java нет сложностей ни с читаемостью, ни с парсингом типов.

Не думаю, что стоит сколь-либо серьезно относиться к комментариям на опеннете — там такая атмосфера, которая привлекает либо троллей, либо очень, так скажем, странных людей.


Претензии к синтаксису обычно возникают у тех, кто программировал на одном-двух похожих языках, и сводятся к тому, что непривычно.


На самом деле к любому синтаксису привыкаешь через 3-4 недели активного использования языка, и то, что казалось странным, перестаёт таким быть и становится совершенно естественным. А объективные, действительно обоснованные претензии к синтаксису могут быть после минимум полугода опыта активного программирования на языке, но за это время обычно и к объективным недостаткам привыкаешь — ну вот просто тут оно так, в других языках другие недостатки, ну и что :)

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


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


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


Бескомпромиссная выразительность замедляет понимание кода, а ускорение понимание кода делает его многословным. И невозможно в рамках традиционного дизайна ЯП разрешить это противоречие.


Создатели Раст, как и любого другого языка, заявляют что они якобы нашли наилучший компромис, а на самом деле диктуют свою волю/видение того что лично они считают правильным/удобным/лучшим синтаксисом. Это как спор о том что мои настройки водительского места для моего тела лучше чем все остальные, только речь о мозгах — мой набор привычек лучше вашего.


Мне кажется есть другой путь, другой дизайн ЯП который может дать больше если и не полностью разрешить конфликт чтения/написания моих/чужих предпочтений то значительно сгладить эти противоречия.

Много пафоса, мало конкретики. Покажите язык с "разделением написания и чтения на уровне дизайна", а мы попробуем оценить преимущества.

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

ПС
Хотя сам признаю, что конкретный пример скорее неудачный. Потому, что в Java не только писать многословно, но и читать избыточные интерфейсы неудобно.
Я скорее про то что мог бы иметь в виду комментатор.
Мои мысли, учитывая другие комментарии здесь:
1. Обсуждать синтаксис языка — это нормально, «шашечки или ехать» тут не при чем. Дизайн языка связан с его синтаксисом, все вместе влияет на эффективность работы программиста, поэтому это важный параметр, особенно если говорить о более менее объективных вещах, а не о "<> vs [] для дженериков".
2. «Токсичность» здесь — это такой художественный оборот, ничего ужасного в этом слове нет.
3. Никогда не встречал проблем с синтаксисом раста, проблемы были исключительно с семантикой — особенно в вопросе заимствований и лайфтаймов. К синтаксическим особенностям адаптируешься очень быстро, тем более что синтаксис вполне похож на мейнстримные языки, просто с дополнениями. Это ж не МЛ и не лисп, чтоб заново внутренний парсер прокачивать на полностью новом синтаксисе.
Отойдя от C(++) и rust: часто работаю с амазоном, и там есть такая штука для автоматической выкатки инфраструктуры как CloudFormation. И оно понимает 2 формата, json и yaml. Если кто не знает — json это а-ля С++ стиль, с кучей {}, а yaml — а-ля питон, с отступами пробелами. Когда я пришёл в проект, 2 человека работали в формате json, но для меня это была боль. Перенёс блок кода, всё сломалось. Где-то одна из скобочек потерялась, а ругань валидатора там в стиле «плюс-минус в этом файле потеряли скобку». Да, работаю я обычно в vim или блокноте, поэтому не ощутил мощь IDE которые (обычно без проблем) умеют в перемещение блока.
Потом я открыл для себя cfn-flip, и у меня появился локальный гит уже с yaml версиями, которые перед коммитом в основную репу надо было конвертировать назад. К счастью, появился ещё новый сотрудник, который тоже оценил всю мощь yaml и мы убедили старую команду что пора переходить глобально. Бонусом там ещё ряд плюшек появился, вроде поддержки комментариев и упрощённый синтаксис, но это уже оффтоп.
И огромным бонусом 2 момента:
— Код стал в 2 раза меньше по строкам и короче в длине строк
— стало удобно работать в любых текстовых редакторах, а не только в «специальных IDE»

И пока писал, вспомнил про свой старый опыт общения с программистами того же php — форматирование текста компилятору не важно, поэтому ВЕСЬ код мог быть как в одну строку, так и разными, но без отступов, РАБОТАЕТ ЖЕ. А python-стиль всё-таки ломается без форматирования, так что другим людям с кодом можно работать без применения IDE, которые переформатируют код как нужно (и привет системы контроля версий, для которых это уже новый код).
Такие дела.
У меня с YAML обратная история: стоит немного ошибиться с отступами и вся конструкция летит к чертям. Такое, кстати, часто случается как раз при копирования со страницы примера в vim.
Когда писал в YAML код с достаточно большой вложенностью (Ansible, Kubernetes, Docker Compose и т.п.) как раз форматирование отступами добавляло боли.
Для того чтобы работать с многочисленными скобками, достаточно вспомнить что у vim есть команды перехода по скобкам и сворачивания блоков. А также подсвечивание скобок.
JSON — это не С++ подобный стиль — это валидный Javascript.
Это и валидный Python тоже.

Кстати, валидный JSON — это ещё и валидный YAML.

Ну отсутпы часть структуры, точно так же если не написать скобки в других форматах будет ошибки. В целом, с отступами кажется что труднее набирать, но по факту не особо. По крайней мере если есть редактор который понимает yaml и умеет например сам отступы создавать для подуровней. Или когда вы пишете "минус" сразу понимает что щас будут перечисления масссивов и автоматически выравнивает текст "на основании текущего уровня вложенности". Хотя для достаточно больших файлов мб и неудобно, больше нескольких сотен строк не было нужды писать.

Ну так в том-то и проблема, что они часть структуры, но при этом не всегда корректно копируются-вставляются.


Кстати, говоря о форматах конфигов, мне как-то последнее время очень TOML нравится...

Даже при копировании-вставке IDE обычно правильно поправляет форматирование: вставляет или убирает нужное количество вложенности.


В общем, в теории проблема есть, на практике не встречал: если в питоне проблемы ещё бывают из-за интерпретации, то с ямл или там хаскеллем уже нет.

стоит немного ошибиться с отступами и вся конструкция летит к чертям.

При этом
1) валидаторы плюс-минус 1 строка говорят
2) если везде отступ 4 пробела то вдруг 8 пробелов — чётко строка понятна. А с этими скобочками, когда УЖЕ потеряли одну — у меня часто ругалось плюс-минус 100 строк, а часто просто на конец файла.

Когда писал в YAML код с достаточно большой вложенностью

Писал часто и много. Вообще нет проблем. Тем более с pip install yaml-lint; yaml-lint file.yaml

у vim есть команды перехода по скобкам

Есть, но я вот не знаю «скопировать блок до закрывающей скобки, чтобы осталось валидно», а просто переход через # — это максимум поиск, но не дальнейшая работа.

А также подсвечивание скобок.

чтобы увидеть закрывающую скобку, нужно встать на открывающей. А часто это не один экран, особенно когда встали на начало файла в 100кб…

Спасибо за развернутую статью, сохраню себе
Каждый новый язык, когда он появляется на свет — он дитя своего времени
Не думаю, что можно ругать синтаксис Assembler, Forth и Pascal )
Очень большая разница: в отличие от Forth, C, Perl и т.п., созданных инженерами для инженеров, Pascal разрабатывался именно ради читаемости кода — профессором для использования в своём курсе.
… часто хотят, чтобы программа выглядела подобно тексту на естественном языке. Но такой формат совершенно неудобен для записи программного кода, и вот почему: текст на естественном языке — это просто запись последовательного потока речи, тогда как текст программы — это чаще выражение структурных отношений между элементами кода.


На мой взгляд такое понимание — это заблуждение программистов, хотя и вполне объяснимое. Более того, существует не просто идея такого языка, но и практическая реализация — язык макрокоманд Access. Конечно, эта реализация «мало чего» дает, но для тех кому само собой программирование просто «без надобности» это очень удобно, если нужно просто порезвиться в рамках такого «мало чего», то — не нужно ничего особо изучать, просто понимать как называются объекты в Access — таблица, форма и т.д.

Или, если перейти к философскому осмыслению, то, наверное, не изучал что такое программирование с семантической точки зрения; как можно предположить, оно вряд ли далеко ушло от обыденной семантики и в любом случае, в рамках решения тривиальных задач, а не «тяжелой» математики может допускать перевод и на язык тривиальной семантики.

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


Есть языки создававшиеся специально для обучения и простые именно для неискушённого пользователя. Есть языки с низким порогом входа, с минимумом ограничений, для набрасывания прототипов, когда "ещё ничего не ясно" и нужно много экспериментировать. Есть совсем простые скриптовые языки как раз для целей "выполнить по очереди несколько команд не заморачиваясь". Бейсик — он где-то посередине между этим всем.


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

Комментарий совсем о другом. Обычное письмо можно реализовать посредством иероглифов и посредством знаков алфавита. А у программистов есть расхожее предубеждение, что программы невозможно писать как нарратив, как рассказ. Эту иллюзию развеяли создатели языка макрокоманд Access, фактически строящегося как рассказ. Это предубеждение проскользнуло в посте, а я возразил.

Хорошо, давайте переформулирую так: возможность — не значит практичность и пригодность во всех случаях.


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


Ну, я конечно не могу не согласиться, что есть случаи, когда подобный формат написания программ работает и даже предпочтителен (целевая аудитория — легче воспримет такое без дополнительного обучения). Я с этим не спорю. Есть и более наглядные вещи, которые делают для детей. Просто это ну совсем разные ниши и в контексте сложных языков и задач — всё меняется. С ростом числа условий, который нужно описывать и читать, многословность начинает всё больше мешать.


В сообществе разработчиков Rust и всех сочувствующих (процесс разработки, к слову, открытый) — постоянно поднимается вопрос, насколько "вербозным" или компактным должен быть язык, как только начинают обсуждать синтаксис новых фич. И есть такое противоречие: человек, сталкивающийся с какой-то фичей очень редко или вообще впервые — хочет, чтобы она была более самообъясняющейся. Человек, постоянно работающий с языком, хочет многие вещи более компактными, чтобы они не отвлекали от сути и позволяли уместить в поле зрения более широкую картину.


Так-то и в Rust можно на основе синтаксического дерева целую повесть написать, заменив каждый знак на какое-то формализованное, читаемое и человеком, и машиной, предложение. Продуктивность одних — возрастёт, продуктивность других — упадёт. Чья продуктивность нам при этом важна и на какие компромиссы мы готовы пойти? Где золотая середина между блоксхемами с комиксами и заклинаниями в стиле K/J?

Ваши размышления, я могу только одобрить, к ним у меня нет возражений. Но за исключением одного частного момента — я никогда не записывал макросы в Access как последовательности действий, всегда придумывал «логику» этих действий до того, как их выполнял. Да и само собой в макросах Access существенное место занимает такая команда как «выполнить запрос SQL», который до того еще следует построить. То есть, в том числе, это и средство связывания между собой разных запросов. Или — в моем случае действие внесения записи, когда эта запись сразу распределяется по трем таблицам, это выполнение десятка запросов.

А второй момент, что мне в этой проблеме важно — программирование это такая сложная форма или вид деятельности, что «недоступна для непосвященных». Но если взглянуть более внимательно, то сама причина такой недоступности — не сложность самих операций, если принять во внимание набор простейших действий, а необычная семантика. Если изменить семантическую часть, то есть — только обозначения, не меняя самой функциональности, то такой язык можно будет осваивать, не прибегая к специальным знаниям. Это, в общем, идея Н. Вирта, но, я понимаю, ему не хватило смелости вторгнуться в саму семантику, заменить «case of» на какого-нибудь «сторожа». А такая замена, но — не в такой форме, она реализована как раз в макрокомандах, о которых речь. И результат — порог вхождения во владение таким языком чуть ли не нулевой. То есть — мне более интересен парадокс языков программирования именно как семантическая проблема, а не техническая.

Более того, любые попытки построения «облегченных» языков программирования, я пытался познакомиться со многими подобными решениями, это опять повторение тех же структур математической логики, а в макрокомандах — это просто «бытовая речь». То есть программист уверен, что языком «бытовых» понятий нельзя описать последовательность программы, я с этим не согласен.

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

В предыдущем топике, где комментатор высказывал ту же самую мысль, я предложил ему взять любую современную математическую статью на свой выбор, и сравнить, какую её часть занимает текст на естественном языке, и какую — значки и формулы.
Специальные языки упрощают работу в чём-то, но далеко не во всём. Программисты же, в отличие от математиков и юристов, подчинили специальным языкам всю свою предметную область целиком — настолько, что мастерство программиста отождествляется со владением конкретным(и) ЯП.

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

Юристу не менее важно, как составленный им договор будет понят другими юристами; тем не менее, на полностью формализованные языки юристы не переходят.

А это и невозможно в ближайшие лет 50. Дуглас Ленат начал проект формализации здравого смысла (Cyc) в 1984 и конца ещё не видно.

Потому что гуманитарии. Были бы инженерами, то давно бы уже перешли на ложбан.

Полагаю, что ещё при нашей жизни владение формализованными ЯП для программиста станет столь же необязательным, как и владение ложбаном для юриста.

(Да, и юриспруденция — не гуманитарная дисциплина, а социальная — наравне с экономикой и т.п.)
взять любую современную математическую статью на свой выбор, и сравнить, какую её часть занимает текст на естественном языке, и какую — значки и формулы.

Ну не уверен. Так и в этой статье о программировании не много кода по сравнению с естесвенным языком. Если мы хотим сравнить математику с программированием по закорючестистости, то наверно с програмой надо сравнивать не произвольную статью, а доказательство теоремы или решение задачи, и там уже слова на естественном языке служат дополнением с решению, чтобы человек читающий это, понимал, а почему мы делаем это и это и как мы перешли от А к Б. Как коментарии в коде, только красивую архтектуру, как в программировании, не построишь, чтобы избавиться от «комментариев».
Под «математической статьёй» я имел в виду, конечно же, scholarly paper, а не публицистическую статью, как эту. То, что у русского слова «статья» есть оба значения — хорошая демонстрация сложностей при использовании естественного языка :)

Работа математиков заключается в создании статей точно так же, как работа программистов заключается в создании программ; а публицистика не является обязательной частью ни того ни другого. В качестве примера можете взять www.imj-prg.fr/preprints/227.pdf (включённую в список en.wikipedia.org/wiki/List_of_important_publications_in_mathematics) и посмотреть, какую её часть составляют специальные значки. По-моему, где-то половину.

Отличие от комментариев в коде в том, что с удалением комментариев код становится менее понятным, но не теряет смысл полностью.
НЛО прилетело и опубликовало эту надпись здесь
Мир просто столь разнообразен, что возможны такие проблемы, мимо которых все проходят мимо и не думают, что есть такие проблемы. Если для литературы это уже традиция исследовать «как понимают» литературное произведение, то пока на нашлось такого психолога или семантика, который бы исследовал, как понимают языки программирования, причем не только профессиональные программисты, но и многочисленные пользователи, которые с этим иногда сталкиваются.
Нет, конечно я не о синтаксисе; позволю себе объяснить на примере, относящемуся к языку математики.

Есть такой упертый гуманитарий, убежденный, что апория «Ахиллес и черепаха» это что-то существенное; наша задача — развеять это убеждение, и, как ни странно, это возможно. Тогда мы говорим, что и Ахиллес, и черепаха — это не иначе, как фигуративные имена двух закольцованных функций, где само кольцо построено на том, что каждая из функций — это аргумент другой функции. А смысл состоит в том, что на каждом шаге итерации эти функции возвращают другу другу значения, меньшие, чем предыдущие, и, несмотря на это, 0 никогда не бывает, или — «Ахиллес никогда не догонит черепаху». Гуманитарий же в этом случае неизвестно о чем думает, возможно он просто увлечен самой фигуративностью имен.

Точно так же и в макрокомандах Access — можно перевести какую-нибудь «Открыть форму А» в режим модуля и он что-то там напишет на VBA. Я об этом — можно использовать базу именования происходящую из математической логики, а можно — из бытовой лексики, это то же самое, вопрос только в том, что удобнее, кому удобнее и в каких случаях.
Я два года назад загорелся идеей языка Раст и выделил месяц, чтобы изучить его. В целом я разочаровался даже не потому, что какой-то непривычный синтаксис, а потому что это все слишком сложно, всё время ругается статический анализатор. Чтобы написать программу, которая на C++ делается за 10 минут, надо час. То есть со всеми этими оунершипами — очень много возни в обмен на гарантию отсутствия NPE — это слишком дорого. Мне кажется более перспективный язык — v-lang, он еще сырой, но концептуально это как раз то что нужно — автоматическая сборка и за счет этого скорость. Для тех объектов, которые не могут зациклиться — подсчет ссылок, для остальных — анализ циклов.

В обмен на гарантию отсутствия не только NPE, но и UB на ровном месте. Вообще, после нескольких месяцев интенсивного использования Rust мозг перестраивается и борьба с borrow-checker прекращается. Дело привычки.

Несомненно в C++ куча мест где можно на пустом месте схлопотать UB. Если бы только эти дыры заткнули, было бы уже значительно лучше. Мне кажется, что вся эта свистопляска с borrow cheker-ом, она больше проблем создает чем решает в смысле цены разработки софта.

Ну вот вам раст эти дыры заткнул, а вы недовольны что "всё сложно и анализатор постоянно ругается" :shrug:

попробуйте Nim, в него уже кстати завезли borrow-checker, но не такой строгий как в Rust

но не такой строгий как в Rust

То есть никакой. Нельзя быть немножко unsound, также, как нельзя быть немножко беременной.

Это у вас IT-софистика. Но намекну что вы будете делать тоже самое в расте, только руками делая clone или расставляя Rc

Судя по Вашему описанию, это напоминает не borrow checker, а garbage collector.

Посмотрите вот эту статью и видео ради интереса — https://habr.com/ru/company/rambler_group/blog/533268/


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


А вот если взять уже программу которую на Питона надо писать хотя бы 1000 часов…
То внезапно окажется что такую же на Раст можно написать за 700. Потому что экономится куча времени на дебаге и рефакторинге.

Посмотрел статью, описанного вами вывода я там не увидел. Давайте возьмем пример первый из статьи:


from typing import List

def last(items: List[int]) -> int:
    return items.pop()

Переписали:


fn last(mut items: Vec<i32>) -> i32 {
    items.pop()
}

Видим, что типы не совпадают — поп из пустого вектора вернет None. И тут у нас есть два варианта: в серьезном аппе мы конечно же вернем опшн, выше где-то уже будем проверять что значение есть и дальше писать какую-то логку.


А можно сказать в стиле питона "да и хрен с ним, падай если значения нет" и написать:


fn last(mut items: Vec<i32>) -> i32 {
    items.pop().unwrap()
}

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


Сколько времени нужно чтобы написать анврап? Ну, у меня это заняло секунды три. Хотите так писать — пожалуйста, никто не запрещает. Так что даже вот эти "10 минут на питоне — час на расте" верно только для сценария "пишем идеоматично". А если "пишем как на питоне" то будет минут 15. С такими же рандомными отваливаниями в рантайме "ой, вместо числа была строчка", но зато быстро.

Этот вывод дают докладчики в своем видео… Понятно что с простыми примерами эффект не так заметен. Как раз там они и говорят что он начинает заметно проявлятся именно на достаточно большой кодовой базе.

Так что даже вот эти «10 минут на питоне — час на расте» верно только для сценария «пишем идеоматично». А если «пишем как на питоне» то будет минут 15.

Если писать «как на питоне», то какой смысл делать это на Rust?

Можно забивать гвозди отвёрткой, но молотком будет быстрее, а отвёртка полезна совсем для другого.

Чтобы не разводить 10 языков на проекте, например. Ну или растовик на расте наскриптует быстрее, чем на питоне если он его не знает. Мало ли какие сценарии бывают.


Вообще я лично для скриптов шарп юзаю: он менее заточен на корректность, но ноги отстрелить не дает.

А можете привести пример, что вы пытались сделать и как компилято ругался? Потому что честно говоря мне не очень легко представить ситуацию где на хелловорлдах компилятор будет как-то ругаться. Сложности начинаются только при попытке сделать уже что-то более-менее продвинутое: потоки посоздавать, тип дерева сделать и вот всякое такое.

Из моего опыта:
При написании даже несложного асинхронного кода можно получить очень странные ошибки которые связаны с попыткой переноса типов не реализующих Send через точку await (например обычный синхронный MutexGuard). ИМХО в этом месте компилятор выдает очень странные и неочевидные сообщения об ошибках до понимания которых я дошел только сильно позже когда разобрался как именно работают Futures.

Ну "несложный асинхронный" в расте это просто оксюморон. Человек который начинает знакомиться с япом вряд ли пойдет сразу в асинки, тут и более подготовленному человеку утонуть можно. Хотя, если так и было, то претензия понятна. Но скорее схожа с попыткой "просто прыгнуть по случайному адресу в Java, ведь в ассемблере это у меня было одной из первых лаб!". некоторые вещи просто не делатся просто в каких-то других языках.

Можно я приведу?
Оговорюсь: у меня от Rust'а неоднозначное ощущение. Не такое как у автора корневого коммента, скорее: «если изучить Rust, то на C++ начинаешь более осознанно писать более безопасный код, но сам Rust я не очень понимаю к чему: для мелких утилит я всё держу в голове. Для достаточно больших проектов — оверхед написания, уже весьма большим получается (и это ещё мне непонятно, насколько вообще ± системные хуки писать можно)».

Так вот есть ring зацикленный список (желательно с головой).
На мой взгляд он востребован почаще двусвязного списка. Так вот реализовать этот самый ring на Rust — задача которая у меня не получилась (разумеется я только изучаю Rust). Например я вообще не понимаю где там WeakPtr ставить.
НЛО прилетело и опубликовало эту надпись здесь
Спасибо.

Отмечу только, что это было не «помогите написать односвязный список»: я интересуюсь Rust'ом не по работе, так что наберу language mastery понимание придёт.

Это был ответ PsyHaSTe который просил пример «пример на что компилятор ругается». Вот пока не наберёшь достаточную language mastery (не знаю как каноничнее выразится) — много сложного.

А когда наберёшь — а чем это будет принципиально отличаться, от С++ (я утрирую конечно, но насколько сильно?):
— C++: не используем всю мощь языка, а кодим конкретными «относительно безопасными» шаблонами, где вероятность UB минимизирована (как минимум под наш компайлер)
— Rust: не используем всю мощь структур данных, а кодим конкретными, легко реализуемыми в Rust шаблонами где наткнуться на спор с borrow checker невелика.
НЛО прилетело и опубликовало эту надпись здесь
Да согласен.

А вы можете оценить, сколько реально стоят, в % скорости разработки, эти гарантии компилятора (к тому же абсолютных гарантий не бывает)?

От себя замечу, что в моей практике UB — скорее раздражающий фактор (2 раза в год по 2 суток отлаживаться — 2% времени разработки), чем сильно сказывающийся на скорости разработки.
Есть вроде такой психологический эффект (эволюционно он понятен), когда редкое, но сильное вознаграждение \ наказание — запоминается непропорционально сильно реальному влиянию.

Также добавлю, что в моих реалиях ad-hoc полиморфизм востребованее, чем traits. Хотя «идейно» traits конечно, прекрасны.

Update:
PsyHaSTe
Также: а насколько эти «только две сложные концепции» отражаются на скорости разработки?
Также: а насколько эти «только две сложные концепции» отражаются на скорости разработки?

Ни насколько: для решения первой проблемы я все заворачиваю в Arc, для второго есть готовые либы — арены, графы, списки — на любой вкус и цвет.


Но писать свои нетривиально, да.


От себя замечу, что в моей практике UB — скорее раздражающий фактор (2 раза в год по 2 суток отлаживаться — 2% времени разработки), чем сильно сказывающийся на скорости разработки.

Проблема когда за 2 дня простоя компания теряет миллионы — раз. Если вы читаете PVS то должны знать, что количество УБ которое попало в прод != реальному количеству уб, потому что никто не знает, сколько времени разработчик в итоге писал код, а сколько — дебажил и пытался понять почему код не работает как нужно. В итоге в прод-то допустим код попал без УБ, но вот провозился с ним разработчик куда дольше чем мог бы.


Недаром чем сложнее проект и больше компания, тем больше всяких линтеров и правил в ней. Просто потому что так легче. Сложность от них растет линейно или медленнее, а сложность от комбинаторики всяких факторов — экспоненциально. Например, я практически уверен что написать свой браузер или ОС на расте проще, чем на питоне без тайпхинтов.

НЛО прилетело и опубликовало эту надпись здесь
От себя замечу, что в моей практике UB — скорее раздражающий фактор (2 раза в год по 2 суток отлаживаться — 2% времени разработки), чем сильно сказывающийся на скорости разработки.

Think of "it works" when you have UB like this: You've flipped a coin 1 time and it's come up heads and you've concluded it's never tails..


Если вы не нашли UB у себя в коде, то это ещё не значит, что его нет.

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


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


Написание кольцевого буфера — это не новичковая задача в расте, вот и все. В плюсах вот новичковая, а в расте — нет. Потому что новичковый буфер в плюсах не обязан работать всегда, а раст заставляет проверить все корнер кейсы. Что будет с новичковым буфером если из двух потоков попытаться в него вставить ноду? "Да у меня такого не будет, я же только учусь" скажет новичок и будет прав. А компилятор раста обязан учитывать такую возможность, ведь он должен предоставить гарантии которые на главной странице сайта япа написаны

Я думаю мой ответ на ваш комментарий будет точно таким же.
habr.com/ru/post/532660/#comment_22547996

Насколько эти «всего две сложные концепции» отражаются на скорости написания кода \ сложности поддержки когда вы уже достаточно освоились с языком?

— Rust: не используем всю мощь структур данных, а кодим конкретными, легко реализуемыми в Rust шаблонами где наткнуться на спор с borrow checker невелика.
В Rust структуры можно комбинировать
Arc<Mutex<Foo>>
Потому из конкретных, реализованных в стандартной библиотеке, можно собрать много всякого.

Опишите какое апи вам нужно, попробую в 50-100 строк что-нибудь рабочее предложить

НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
Моя программа good enough, я запустил ее с валгриндом, ничего не падает, мне этого достаточно. Растовая программа с тем же успехом может вылететь по out of bounds или грохнуться в анмэндж/нативном коде.
Моя программа good enough, я запустил ее с валгриндом, ничего не падает, мне этого достаточно.

Вы же понимаете, что тестирование может показать наличие ошибок, но никак не их отсутствие?


Растовая программа с тем же успехом может вылететь по out of bounds

… Там, где аналогичная программа на C++ портит память.


… или грохнуться в анмэндж/нативном коде.

Надо же, совсем как C++!

Вы же понимаете, что тестирование может показать наличие ошибок, но никак не их отсутствие?

В точности, как и все без исключения остальные техники проверки корректности более-менее сложного кода.


Надо же, совсем как C++!

Ну да, вам ровно про то и говорят же. В точности, как c++.

А что такое «анмэндж/нативный код» у Rust?

Чтобы написать программу, которая на C++ делается за 10 минут, надо час.

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

Так что нет, это не «слишком высокая цена», просто у раста кривая обучения не похожа на иные языки. И это известный факт.

Даже находясь в верхних 5% на этой кривой, вы будете писать значительно дольше чем программисты на 90% других языков, находящихся в 5% в своих языках.

Нет конечно. На js сам язык вытрет за мной мусор. На C/C++ я сам должен вытереть, от этого программы становятся больше и писать их дольше. На Rust я должен доказать компилятору что с памятью будет все ОК. Поэтому на раст программы получаются еще более сложными и написание еще более тяжким. Стоит ли оно того? Если программируешь кодек какой-то, хайлоад вебсайт или космический корабль — наверное да, не исключено. Однако для огромной части задач эта сложность программирования не спасает от других багов, а цена работы растет.

Я что-то пропустил или в программировании все задачи, кроме управления памятью, уже решены?

у раста кривая обучения не похожа на иные языки. И это известный факт.

Кому известный? На какие «другие»? Чем именно не похожа?


Раст — всего лишь еще один низкоуровневый язык, появление которого бы вообще никто не заметил, если бы не хайп по типам и не агрессивный маркетинг. Даже Julia извне пузыря — выглядит в разы более продуманной. Через несколько лет спираль сделает очередной изгиб — вот тогда и посмотрим.


Говорить про что-то, в чем сам уже освоился, мол «это сложно, не всем дано кривая обучения особенная» — очень смешно и не очень вежливо. У понимания доказательства гипотезы Римана — кривая действительно крутовата. У очередного говноязыка, каких было и будет сотни — никакой особенной кривой нет, код пописал месяцок — и уже мидл.

И да, и нет. Смотря с чем сравнивать. Если посмотреть на континуум используемых языков, то раст в стороне в более сложной области, где-то наверное на полпути от мейнстрима к хачкелю\скалке. Если бы это был инопланетный язык который никто не знает (Как какая-нибудь агда), тогда разговора бы и не было, потому что мы берем не все языки, а только на которых реально работу найти хотя бы если ищешь.

в более сложной области, где-то наверное на полпути от мейнстрима к хачкелю\скалке

Шутите, что ли? Вряд ли существует более дубовый и простой с точки зрения синтаксиса язык, чем хаскель. У него высоченный порог входа из-за дикого хлама в prelude и миллиарда способов чихнуть в любом месте. Так-то только лисп проще. Скала без дикого синтаксического сахара — абсолютно внятный и очень красивый язык с минимальным порогом входа; да гиковский сахарок постепенно превращает ее во write-only сущность, но это никак не повышает порог входа, опять же. Можно легко проработать десять лет на скале и думать, что это котлин, если не лезть в дебри.


Я лично не вижу ничего концептуально сложного в расте потому, что там ничего такого и нет. Даже трансформации данных в R, или неявные преобразования типа в COBOL требуют куда большего умственного напряжения при попытке их понять, чем весь раст вместе взятый.


Так что нет, не убедительно. Синтаксис, как синтаксис — не лучше, и не хуже остальных.

Дело не в синтаксисе, а в концепциях. HRTB например это rank-2 лайфтаймы. Не только лишь всем легко в этом разобраться, разработчики команды C# говорили что значительную часть аудитории составляют люди которым обычные генерики сложно, и от struct Array<T> у них уже голова болит.


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

НЛО прилетело и опубликовало эту надпись здесь

Да, я упростил «as in ghc со 100500 расширениями костылями» до «дикого хлама в prelude и миллиарда способов чихнуть» потому что ноги костылей растут именно оттуда.


Разумеется, на идрисе писать проще (для агды мне надо освежить нотацию, но не думаю, что это станет прямо порогом, просто пока соль была). Проще потому, что идрис разрабатывался с учетом родовых ошибок хаскеля, и даже небольшой рефакторинг пакетов 1 → 2 не вызывает ни одного wtf. Ну а еще — [nb непопулярное мнение] — проще потому, что зависимые типы — это очень изящная, внятная, полезная, целостная и непротиворечивая парадигма. А типы, как в хаскеле — это попытка натянуть сову на скелет мамонта.


В хаскеле (и всем остальном независимо типизированном, привет, Куба) — типы это помощь компилятору, которая на каждом шагу вставляет палки в колеса разработчику. А в идрисе/агде — это помощь разработчику, милостиво предоставляемая компилятором.


Модуль один, но какие разные векторы!

НЛО прилетело и опубликовало эту надпись здесь

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

Не имеют аналога === прямо заимствованы из c++

Покажите мне, пожалуйста, рабочий анализатор времён жизни для C++. Только не надо про -Wlifetime, он сломан.

НЛО прилетело и опубликовало эту надпись здесь

Слушай, я прекрасно понимаю, что у тебя ситуация другая. Но ты не часть рынка, ты "вне" его. Я вот с недавних пор кручусь в около-ФП тусовке, так лица одни и те же везде. Мой коллега из чатика со смехунчиками нашел работу на хачкеле и оказывается что он теперь работает с другим чуваком который админит канал по завтипам (и с которым я заочно тоже давно знаком) и с третьим чуваком, с которым я знаком через третьи руки. Даже не завтиповой, а просто ФПшный мир очень и очень тесен, он на порядки меньше какого-нибудь "жаба ворлд"а. Это мне напоминает статью на хабре, где челик сказал как легко без гринкарты попасть в штаты, достаточно иметь 3-5 публикаций в журнале и тебя по О визе перевезут. Блин, если бы у меня были скиллы публиковаться в журналах я бы наверное на сишарпе бы и не писал.


Такое ощущение, что я просто слишком глупый для этого. У меня нет достаточного уровня ненависти к себе чтобы идти в банк или заниматься HFT на плюсах. При этом у меня недостаточно мозгов чтобы заворачивать топосы в левые сопряженные, я не МГУ заканчивал, а средненький московский ВУЗ с преподами за 70 преподающих GPSS и фортран с паскалем, из которого я вынес разве что умение искать информацию. Чего в целом хватает чтобы иметь ЗП выше среднего во МСК (хотя и существенно если не в разы ниже условных сберов), но базы для роста куда-то в науку не дает, тем более что тратить уйму времени впустоту не получается, мотивация под ноль скошена непонятностью перспективности такого направления.

НЛО прилетело и опубликовало эту надпись здесь

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

А он тоже вполне может быть ВАК-аккредитованным или вообще даже быть в Scopus/WoS.

Хмм, а как узнать? Особенно если я не помню точно его длиннющее название.
НЛО прилетело и опубликовало эту надпись здесь
Хай, родной — оцени уровень ненависти к себе — у сбера есть школа 21 и там есть люди что учатся. Тобишь они идут дофига времени учится у сбера чтобы потом работать на сбер. Фаталити! :) Если серьезно — зря ты так банки ненавидишь.

А у меня так же пригорает от синтаксиса go. Чего стоят только бесконечные if err != nil, мутные тестовые фреймвор и ошибки компиляции при неиспользуемое модуле, или переменной, мешающие экспериментам.
Если бы мне не платили, я бы не стал им пользоваться.

Я бы не стал им пользоваться даже если бы платили.
К сожалению, когда заходит речь о командной работе, где у большинства глаза никогда не горели в этом направлении или вообще, Go не имеет альтернатив, если нужно компилируемое standalone приложение. Люди не примут такую кривую изучения Rust, для многих она окажется слишком крутой.
так где-то была инфа что большинство любителей Rust не пишут на нём а любят кагбэ «заочно»

Rust's adoption issue surfaced in January's Stack Overflow's 2019 survey, which revealed that despite developers' positive feelings toward Rust, 97% of them hadn't actually used it.

источник

было бы круто если бы хабровчане которые пишут на Rust пошарили инфу что они пишут на Rust, какие инструменты используют, какие библиотеки, с какими проблемами сталкивались (я не верю что проблем нет), как их решали и тд.
Rust as a language doesn't really have an opinion on how to do concurrency or parallelism. — https://doc.rust-lang.org/nomicon/concurrency.html

В 2020 году сие звучит даже хуже, чем оригинальная кооперативная многозадачность в Go. Я просто не представляю себе, насколько оторванными от реального мира нужно быть, чтобы с таким подходом браться за создание языка в XXI веке.

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

Про верифицированные многопоточных примитивы поподробнее plz. А async/await уже нормально работает, или как год назад — match неожиданно (и для самих создателей) захватывает контекст всего "верифицированного" многопотока?

Ну вот список верификаций: https://plv.mpi-sws.org/rustbelt/#publications


В качестве примера вот одна где во время верификации обнаружили баг в реализации атомнного референскаунтера: https://www.ralfj.de/blog/2018/07/13/arc-synchronization.html (ну и сам баг)

Я знаю кто такие Rustbelt и чем они занимаются, но я не в курсе что они уже доказали то о чём тут разговор.

А я что-то про растбелт говорил? Была просьба показать "показать поподробее" — я показал поподробнее.

В официальной документации написана ерунда? — Охотно верю. Нужно рыться в клоне npm, чтобы найти работающую с многопоточностью библиотеку? — Верю еще охотнее.


«Верифицированные многопоточные примитивы» — звучит обалденно круто, как насчет вот такой простой задачки: я хотел бы в 2020 году иметь возможность запустить по гринтреду на каждую FSM у себя в коде (десятки, сотни тысяч, миллионы) и пойти заниматься своими делами, пока мои миллионы транзакций там обрабатываются (они зависимы друг от друга, разумеется, ивент в одной может триггернуть state change в других, или создать новые). Если надо — то по хэшрингу раскидать выполнение на несколько машин в кластере. В идеале — не с помощью какой-то левой библиотеки, а средствами языка.

То, что вы описали — это не многопоточность, а распределенная система акторов. И нет, даже в 2020м году наличие распределенной системы акторов не является чем-то критически важным для языка программирования общего назначения.

Зачем иметь акторный фреймворк "в языке"? Кому и невеста кобыла, у кого и язык без ХТТП в стд — неюзабельный. В расте есть актикс который делает ровно то что вы сказали, занимает топ место в бенчах и вообще прекрасен. В чем проблема, в том, что раст не проповедует "all batteries included", а вместо этого продает "вот вам пакетник и экосистема отличных либ"? А вы в курсе что например был такой крейт hashbrown который предоставлял хэшмапу, и он настолько хорош был, что его затащили в СТД и стандартная хэшмапа начиная с опредленной версии это просто тонкий адаптер (чтобы обратную совместимость по АПИ сохранить) над этой либой? Или вот регулярки: их нет в языке, для регулярок нужно ставить пакет regex. То же самое кстати с каналами — те которые в СТД хороши, но потом появился пакет crossbeam который намного круче и по апи, и по функционалу получился, и его щас тоже понемногу стабилизируют и затаскивают в СТД.


И это не потому, что кортима говноделы которых каждый Васян со свободным временем на коленке обыграет и напишет либу лучше, а потому что экосистема поощряет написание библиотек. "Не с помощью библиотеки, а в самом языке" — единственный такой язык который я знаю это го, и по состоянию на 2021 год в нем есть ровно 0 (ноль) удобных кастомных коллекций или других подобных базовых вещей. И не потому, что СТД такая прекрасная (вспомнить одно только поведение файловой системы го на винде где нет пермишнов rwxrwx на файлах), а в том числе потому что язык не дает никакой возможности спроектировать удобное апи для достаточно абстрактных вещей вроде "мапа любых типов, умеющих хэшироваться" или "тип который можно безопасно переслать в другой поток".


По апи я актиксом более чем доволен. По перфомансу он стабильно занимает первые места в бенчах. В общем, любо-дорого, а не фреймворк. Но вот без шильдика "в стд" видимо придется забить на все это и есть кактус на го :/

Зачем иметь акторный фреймворк «в языке»?

Затем, что без него параллельные вычисления толком не построить. Заметьте, я нигде ничего не говорил про модель акторов, я описал повседневную задачу. И тут же мне хором стали говорить про акторов. Хотя это лишь один из вариантов решения. ОК, пусть надо просто обсчитывать входной поток данных и сплевывать промежуточные результаты вовне. Задача повседневная, уши модели акторов из нее уже не так торчат. На современном языке программирования такая задача должна отнять час на подумать и 10 минут на написать 20 строчек.


Я совсем не топлю за «все должно быть в std», наоборот. Просто некоторые основополагающие вещи не должны зависеть от того, что завтра в компилятор добавят оптимизацию и ваша сторонняя библиотека поломается.


Кстати, отвечу заодно и на


достаточно иметь 3-5 публикаций в журнале и тебя по О визе перевезут

я не знаю, что такое O-виза, и письма из-за океана — выбрасываю, не читая, но кроме публикаций в журналах еще бывает OSS, SO, даже заметный блог — уже почти достаточно, чтобы начать получать предложения. Я даже просто в твиттере постоянно вижу проскакивающие вакансии на весьма экзотических языках (и не только, просто в шарпе/джаве сложнее стать заметным; впрочем, в руби мне как-то удалось).

Речь про США; чтобы получить туда рабочую визу, люди уже с оффером от работодателя могут годами ждать благосклонности миграционной службы.

Наверное, такое тоже бывает. Опираясь на собственный опыт, могу сказать, что вот моя жена не брезгует летать по работе в Штаты и ни разу проблем ни с визами не было, ни от предложений завтра оформить рабочую — отбоя нет.

Не очень понимаю про «завтра оформить»: H-1B разыгрываются раз в году, в апреле.

Понятия не имею, если честно. Может быть, это как-то решается, типа «до апреля по обычной, потом так», может быть имеется в виду «со следующего апреля», может быть компания врет (но это вряд ли, это large cap).


Только кто ж по доброй воле в XXI веке туда поедет-то?

НЛО прилетело и опубликовало эту надпись здесь
там надо год работать в зарубежном относительно США офисе

А, во, это оно, наверное. В зарубежном офисе она уже несколько лет как.


а куда?

Не, ну каждый по себе выбирает, конечно, но есть же места не настолько людоедские, хоть та же Канада, если про «за океан». Я не смог жить в Германии, и почти всем доволен в Испании. Многим Латинская Америка подходит, а иным — Исландия и Новая Зеландия. Люди, слава всему, разные, но как по мне — разницы между Россией и Штатами нет вовсе, просто как страны-близнецы, поэтому странно менять одну на другую.

Люди, слава всему, разные, но как по мне — разницы между Россией и Штатами нет вовсе, просто как страны-близнецы, поэтому странно менять одну на другую.

Эм, можете рассказать, откуда вы сделали такой вывод?

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


Я по утрам читаю новости на четырех языках из минимум шести разных стран, чтобы составить хоть сколько-нибудь правдоподобгую картину того, что происходит в мире.


Так вот, по моим наблюдениям, поведение людей в массе и подходы к государственной политике и пропаганде у наших стран самые близкие.


Как по иному случаю писал Иосиф Александрович, «если выпало в империи родиться — лучше жить в глухой провинции у моря». Так вот империй (пока Меркель не удалось построить пятый рейх в Европе, и пока Китай не потребовал от США госдолг вчерашним днем — но это вряд ли произойдет в обозримом будущем), сегодня на земле ровно две.

не хочу вмешиваться, но ты запутался, про твое утро и жизнь в 90х у тебя спросили где-то в другом треде. А здесь был вопрос, «каким образом между РФ и США нет никакой разницы»
Затем, что без него параллельные вычисления толком не построить. Заметьте, я нигде ничего не говорил про модель акторов, я описал повседневную задачу. И тут же мне хором стали говорить про акторов. Хотя это лишь один из вариантов решения. ОК, пусть надо просто обсчитывать входной поток данных и сплевывать промежуточные результаты вовне.

Пожалуйста: https://docs.rs/actix/0.10.0/actix/trait.StreamHandler.html


На современном языке программирования такая задача должна отнять час на подумать и 10 минут на написать 20 строчек.

ну где-то так и выйдет, да


я не знаю, что такое O-виза

Вот статья

Пожалуйста: https://docs.rs/actix/0.10.0/actix/trait.StreamHandler.html

Наверное, я тупой, но я не очень понимаю, как оно мне поможет из коробки обсчитать ввод в сорока синхронизированных потоках (5 машин, каждая по восемь ядер). Это как бы на сегодняшний день прямо супер-повседневная задача.

Супер-распространенная задача по обсчету на кластере машин чего-либо? Вы требования на ходу выдумываете) Сначала требоавли "возможность запустить по гринтреду на каждую FSM у себя в коде", потом "входной поток данных и сплевывать промежуточные результаты вовне", теперь уже кластер машин. Чуть дальше пойдем — окажется, что у нас миллиарды пользователей, 100миллионов рпц надо выдавать, шесть девяток надежности и вот это все — я правильно понимаю?


Для вашей супер-пупер задачи есть готовые решения вроде мап-редьюса и хадупа, вопрос только при чем тут ЯП?

Вы требования на ходу выдумываете.

Ложь. Я развиваю оригинальную задачу. Если бы вы не выдумали к ней модель акторов в первом комментарии, и не предложили мне костыль во втором (и вообще попробовали бы понять задачу) — вы бы увидели, что ничего в начальных условиях не поменялось. Входной поток бывает плотным, неожиданно да?


[...] шесть девяток надежности и вот это все — я правильно понимаю?

Пятнадцать девяток. А так — да, правильно. Чтобы джейсон перекладывать мне и кобола хватит. Я думал, раст позиционируется не как игрушка для одного тепличного компьютера и разрешением на сбой каждую пятую секунду. Простите, видимо ошибался.


Для вашей супер-пупер задачи

Она уже лет 6 не принадлежит категории особенных. Оглядитесь, что ли.


готовые решения вроде мап-редьюса и хадупа

Все ясно. Спасибо.

Ложь. Я развиваю оригинальную задачу.

"Я не выдумываю требования, а развиваю задачу" — называйте как хотите.


Пятнадцать девяток.

0,000031536 миллисекунд недоступности в году? А вы врунишка оказывается.


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

Ну по сравнению с вашими 15ю девятками ни один язык в мире не отвечает подобным требованиям. Все кругом игрушки делают видимо.


Она уже лет 6 не принадлежит категории особенных. Оглядитесь, что ли.

У гитхаба таких задач нет, у стаковерфлоу таких задач нет,… А это одни из самых популярных и нагруженных публичных проектов.


Все ясно. Спасибо.

Пожалуйста

Если учесть что абзац заканчивается "… weren't willing to commit to for 1.0." то это явно текст не 2020года

было бы круто если бы хабровчане которые пишут на Rust пошарили инфу что они пишут на Rust, какие инструменты используют, какие библиотеки, с какими проблемами сталкивались (я не верю что проблем нет), как их решали и тд.

Тут одним комментарием не обойдёшься, а статью писать лень. Да и не уверен, что тут вообще статья получится. Скажем, я успел над тремя проектами на расте поработать: два блокчейновых и третий о котором пока рассказывать не готов (но не блокчейн). Проекты достаточно разные, сложности больше к предметной области относились, чем к расту. Библиотеки… ну тоже разные. Конечно, serde или tokio сейчас везде, но писать об этом смысла мало.

У растовского комьюнити есть ежегодичный большой опрос, там есть ответы на все ваши вопросы. Можете ознакомиться с ним здесь:
blog.rust-lang.org/2020/04/17/Rust-survey-2019.html

Я лично люблю раст очень даже «очно», пишу на нем почти ежедневно, и за деньги, и для души.

Upd:
Пардон, вот актуальная ссылка за этот год blog.rust-lang.org/2020/12/16/rust-survey-2020.html

А в чем смысл компилируемого standalone приложения в эпоху докера? Или вы десктоп софт пишите для клиентов?

предположу что имеется в виду бинарь где всё включено (GC тоже) и оно просто выполняется. Хотя я так и не понял окончательно, быстрее ли это. Где-то была статья что GC в Go просто потюнали в другую сторону на меньшую latency, и когда java таки «разгонится» и в ней заработает JIT то GO уже не быстрее.
UPD: быстрый старт актуален для AWS Lambda, возможно для таких случаев Go таки лучше

Да, гц го очень разрекламирован но на самом деле довольно посредственный, если смотреть не через маркетинговые очки, а как профессионалы.


Вот хороший цикл обзора state of the art современных ГЦ и почему гошный ГЦ выдает именно те результаты которые выдает:


https://blog.plan99.net/modern-garbage-collection-911ef4f8bd8e
https://blog.plan99.net/modern-garbage-collection-part-2-1c88847abcfd


Но все же поинт "единого бинаря" не очень понятен. В наше время есть "единый докер образ" который точно так же запускается одной командой и тиражируется как угодно.

спасибо за ссылки, почитаю

Но все же поинт «единого бинаря» не очень понятен. В наше время есть «единый докер образ»


ну я думаю там суть не в упаковке
это скомпилированный в таргет платформу бинарь vs интерпретация скажем java кода с помощью jvm. Старт становится быстрее. По идее на этом преимущества заканчиваются.
Бэнчмарки как известно это очень холиварное дело, и java может проигрывать не столько из-за языка сколько из-за всякой магии Spring, аспектов, рефлексии, проксирования вызовов и тому подобного. Наверняка чтобы выяснить что лучше надо мерять конкретные продакш приложения написанные на том и на том.
касательно упаковки — ну может ещё где-то нет докера или оркестратора, у нас был такой случай мы запускали у клиента просто исполняемые файлы

вот кстати довольно занятная (не истина, а просто занятная) инфа по перформансу
www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=composite

Ну так если жит тормозит то берете AOT компилируете (как андроид делает) и порядок, граали опять же всякие.


Но вот например asp.netcore на 6 месте, который обходит только раст и С++ фреймворки. Там конечно специфический бенчмаркоориентированный код, не особо идеоматичный, но тем не менее.


В общем, надеюсь кто-нибудь когда-нибудь мне объяснит смысл "единого бинаря" и чем он так прекрасен. Потому что пока что мы нашим девопсам отдаем просто докерфайлы и репозитории, они запускают docker build . и вообще не знают, сколько там бинарей или не бинарей, и как оно вообще работает. Возможно на всяких лямбдах опыт может быть другой, но мне казалось бы странным что все гошные проекты сидят на авс.

у GraalVM есть свои некоторые минусы, вот статья навскидку dzone.com/articles/profiling-native-images-in-java
в моей практике не было такого что мы использовали GraalVM

Go берут возможно из-за комбинации качеств, типа быстрого старта + хороших (по сравнению с java) возможностей concurrency в ЯП. Я например остро ощущаю отсутствие в java async/await механизма… очень тяжело писать асинхронный код.
По правде говоря, я сам не очень понимаю нишу Go, мне она кажется очень узкой. Это когда «нужно что-то написать простое но ещё не возникло проблем с библиотеками».

касательно AWS Lambda то она довольно популярна в крупных интерпрайзах, в event-driven пайплайнах, когда есть какая-то очередь (Kinesis, SQS, Kafka etc) и лямбда процессит очередь, в big-data пайплайнах тоже, т.к. нередко вместо Spark/Apache Beam берут ту же очередь + лямбда (т.к. разработчикам так проще чем ETL тулзы). Бывает такое что latency важна, скажем для аналитики realtime типа, и в Lambda солидный кусок времени съедает старт кода. Как-то даже про Rust думали, ага.

UPD: Я не знаю сколько реально пишется Go кода для лямбд или контейнеров, просто предполагаю возможные кейсы использования.

ну минус единственный в том, что там свой гц для неподходящий для маленьких куч, то что рефлекшн просто так не заработает было предсказуемо, но забавно, что с конфигурацией даже он заработает, в Net Native емнип от рефлекшна в любой форме тупо отказались. Про то что JIT лучше скомпилит чем AOT это в теории так, на практике все немного иначе. Дампинги и профилировщики — тут тоже понятно, для профилирования не обязательно делать аот, не так часто делается профилирование, можно и пожертвовать "временем запуска".


Лямбда довольно популярна, но я бы не ожидал что прям куча разрабов пишет под нее, а го разработчиков и правда дофига.


Поэтому я соглсен, что язык в теории весьма нишевый, почему на практике столько хайпа, особенно вокруг "единого бинаря" — непонятно. Чай не в 2000 году живем где софт с диска на рабочий стол нужно копировать, и если он portable в единственном бинаре то это сразу +100500 к популярности

НЛО прилетело и опубликовало эту надпись здесь

Ну обычно когда докера нет, то самым разумным первым шагом является поставить этот самый докер. Учитывая что он чуть ли не на кофеварке может запускаться, то это самый простой и зачастую эффективный способ. Я даже десктопные софтины у себя вроде постгреса или опенвпн ставлю через докер, просто чтобы не париться с установкой. Установка опенвпн сервера на комп — 20 листов инструкции, как что сделать. Установка опенвпн сервера через докер — запустить docker run с тремя аргументами.

НЛО прилетело и опубликовало эту надпись здесь

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

НЛО прилетело и опубликовало эту надпись здесь

Можно и без гриночки. Сбербанки нынче платят по 300-400к на руки, это 4-5к евро в месяц или 50-60 в год. Считая налог в европе порядка 30% получаем что это эквивалентно получению 100к евро в год в какой-нибудь Германии. Вполне себе можно жить безо всяких гринок. А учитывая что рынок услуг везде рассчитан на финансовые возможности людей с зарплатой в 500 евро в месяц можно вполне себе неплохо жить.


Но зачем работать в банках? И тот же вопрос про компании с 40летней историей и бюрократическим параличем? Пусть даже они платят деньги и делают гринки.

НЛО прилетело и опубликовало эту надпись здесь

В твоем случае по-моему все сложилось очень даже удачно. Мы +- ровесники, при этом ты посмотрел мир, поработал с кучей интересных технологий, за которые тебе ещё и заплатили, да и если я правильно помню твои комменты про матчинг, 601 и прочее то не обидели, и пусть деньги получились нерегулярными и щас возможно есть какие-то проблемы, это однозначно плюс. У кого-то и 30к накопить это уже достижение. А у кого-то месячный чек.

Качество жизни не во всём определяется толщиной бумажника. Когда на несколько суток отключают интернет из-за выборов, как было у ближайших соседей — толщина бумажника не сильно утешит.

Ну я вот знаю что 0xd34df00d 'у никто не отключал интернет из-за выборов, потому что последние N лет он в штатах живет. Не в деньгах счастье, но без них тяжеловато, и очень многие мелочи из которых оно складывается (вроде вкусно поесть и красиво пожить) без них невозможны.

Вы правильно заметили про роль докера, но упор всё же на компилируемость (в общепринятом понимании) + тащить за собой своё окружение всё же не идеал. Вижу по треду пару упоминаний непонимания Go. Представьте себе проект на платформе Ruby, NodeJs, PHP, Python, чаще всего состоящий из нескольких приложений на одной инфраструктуре. С ростом используемости нижележащие части кода стали хуже масштабироваться, появился смысл вычленять отдельные сервисы. Вот один из примеров. Это может случиться раньше, чем нужно, из-за некоторого хайпа вокруг микросервисов. Многое можно делать на основном ЯП, но почему бы не делать сразу с «запасом прочности»? Заодно с более дешевой поддержкой, т.к. standalone проще развернуть, закинув куда-то единственный инстанс и можно дольше обходиться этим одним инстансом без нужды в масштабировании.
Брать C#, Java только ради быстрой инфраструктуры мало смысла. Основная бизнес-логика, понятно, переводиться на них не будет. Rust тяжело пойдёт команде. Которая ценит наличие типизации, но не особо в курсе глубин гарантий. Go тоже статически типизирован, и из-за этого Go уже тяжело даётся команде, которая в шоке, что нельзя вот так запросто обращаться с JSON. И это могут быть люди, которые уже имеют опыт с MyPy, Typescript, Psalm, но там всегда можно выставить тип «Any» и не париться. =) А команда без опыта статанализа (всю жизнь на динамике) вообще видят типизацию Go в обеспечении надежности как у Idris какого-нибудь. При этом всё это вдвойне касается нанятых позже работников.

Команда на проекте — это не просто набор индивидуальных разработчиков, а так сказать распределённый рой знаний, который пылесосит в себя по мере жизни проекта новые знания. И нужно очень внимательно выбирать, какие технологии пылесосить, чтобы рой не подавился или не стал хуже работать.

P.S. Для обычного приложения с бизнес-логикой по мне лучше вообще брать Haskell или подобные ЯП (Scala, F# — я не особо в курсе). В Rust всё же слишком много крутится вокруг низкоуровневых оптимизаций, под которые оптимизирован и синтаксис. Разве что мне теперь об этом не нужно знать и оно всё равно будет работать.
P.P.S. Для личного кроссплатформенного GUI приложения мне однажды не удалось подобрать ничего лучше, чем Python, из-за хороших биндингов к Qt. =) Выбор был тоже максимально прагматичен.

Не очень понял проблему. Например как у нас: есть полсотни сервисов. Языки: шарп, раст, немного ноджс, пара заброшенных проектов на скале и хаскеле. Каждый сервис идет с докерфайлом. Есть тимсити джоба которая ищет ищет в каждой репозитории свой докерфайл, билдит и деплоит. Ничего про содержимое приложения оно не знает, знает только "если сработал триггера на репозитории вызови докер билд и потом передай вот такие-то аргументы как env'ы приложению при docker run". Список параметров разработчики передают, и в принципе мы пару раз полностью меняли стек (шарп на раст например) так, что девопсам даже джобу не пришлось никак трогать — просто поменяли адрес репозитория потому что под сервис 2.0 сделали свою репу.


Соответственно все ваши Ruby, NodeJs, PHP, Python — совершенно однотипно запускаются и работают. Вот я и не понимаю, в чем ценность этого "единого бинаря". Ведь и без него все работает так же, просто есть "единый образ", который совершенно декларативно описан и just works.


P.S. Для обычного приложения с бизнес-логикой по мне лучше вообще брать Haskell или подобные ЯП (Scala, F# — я не особо в курсе). В Rust всё же слишком много крутится вокруг низкоуровневых оптимизаций, под которые оптимизирован и синтаксис. Разве что мне теперь об этом не нужно знать и оно всё равно будет работать.

Я тоже считаю что хачкель/скала лучше всего при отсутствии каких-либо априорных ограничений на приложение, дальше уже вступает в силу "что лучше знает команда" и "на что есть бюджет". Как раз-таки раст так же как и го по-умолчанию компилируется в единый бинарь, вопрос выше был скорее про альтернативы как раз в виде хачкеля, сишарпа, джавы и так далее.

Ваш стэк кажется более дорогим, чем мог бы быть. По моему опыту эффективнее всего собрать все шишки на одной платформе, и потом её стараться держаться. Иначе будет оверхед в сколько-то процентов человеко-часов.
Docker образ сложнее, чем бинарь Go. Но, в целом, умозрительно дешевле упаковать в Docker на имеющейся технологии, чем вводить этот Go в стэк, да. Это не основной фактор. Задумываться о Go, по-моему, начинают тогда, когда нужно заткнуть проблемы производительности в некоторых местах, так сказать увеличив окно масштабирования.
Хотя некоторые сразу выбирают Go для всего проекта. Скорее всего из-за уже имеющегося опыта с ним, хотя, по-моему, лучше инвестировать время в накопление опыта на другой платформе.
вопрос выше был скорее про альтернативы как раз в виде хачкеля, сишарпа, джавы и так далее.
Ну вот про все эти C#, Java и Haskell, Rust, Go моё мнение в предыдущем комментарии.
Ваш стэк кажется более дорогим, чем мог бы быть. По моему опыту эффективнее всего собрать все шишки на одной платформе, и потом её стараться держаться. Иначе будет оверхед в сколько-то процентов человеко-часов.

Да ничего дорогого :shrug: 100% команды — шарписты, но необходимость покодить в раст или там ТС сервис не вызывают каких-то особых проблем.


Docker образ сложнее, чем бинарь Go.

Ой ли? Докер для дотнета вообще генерируется одой кнопкой в visual studio. Полагаю для многих других языков делается так же. Причем для раста я просто взял докер-образ от дотнет приложения и чутка его подредактировал под свои нужнды. Также сделал для ТС. Там конечно свои нюансы были, но тем не менее. Ну пусть будет "один день чтобы сделать рыбу докер-образа для языка Х". Если у вас в компании не любят на каждый сервис уникальный язык брать, то за 1-2 недели можно наделать шаблонов докер-образов на все проекты в компании. И дальше просто при переводе в докер того или иного сервиса брать рыбу и за полчаса заполнять её нужными данными. Или просто взять язык/иде где докеризация занимает буквально один клик на кнопку "да, сделайте мне хорошо".


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

По моему представлению ГО выбирают динамисты, которым питон оказался медленным, и они с удивлением узнали что можно собирать с +- тем же успехом приложения, но иметь хотя бы минимальную помощь со стороны компилятора из разряда "строки не передаем вместо интов" и иметь многопоток без GIL. В такой перспективе понятна и любовь к interface{}, и хэшмапы черех хэшмапы, и некоторые другие особенности. Не претендую на истину, просто такое вот впечатление сложилось.


Наверно, учитывая невеликий обычно размер Go кода, лучше переписать на что-то более читаемое, чем продолжать бороться, или как-то изначально не выбирать этот ЯП для кандидатов на вырост.

А невеликий это сколько в kloc примерно?

необходимость покодить в раст или там ТС сервис не вызывают каких-то особых проблем
Ну сложности всегда есть, даже на основном стэке. Надо сравнивать производительность (долгосрочную, включая поддержку, не одномоментную). Например, большинство людей имеют тенденцию банально забывать ЯП (даже активно использующийся, какой-то редкий нюанс) и потом гуглить по-новой. Новые люди тем более. На базах данных постоянно такое вижу.
то за 1-2 недели можно наделать шаблонов докер-образов на все проекты в компании
Разумеется, другого равнозначного выбора в любом случае нет, используй Go или нет.
По моему представлению ГО выбирают динамисты, которым… В такой перспективе понятна и любовь к interface{}, и хэшмапы черех хэшмапы
Ну вы примерно повторили мои слова) Хорошо подмечено про интерфейсы, но никак иначе там и не получится. Мне кажется, на своих проектах на динамических ЯП через статанализ на тайпхинтах уже понадёжнее, чем в виденных мной Go программах.
А невеликий это сколько в kloc примерно?
У меня ни в коем случае нет нисколько репрезентативной статистики, но в моём понимании невеликий = <10к. Большинство ведь для всяких микросервисов используют, не так ли? Ну вот тысячи строк наверно можно насчитать. Для меня было недавно открытие, что наш небольшой активно меняющийся проект уже несколько сотен тысяч строк, так что я плохо соображаю в терминах LoC.
Вы правильно заметили про роль докера, но упор всё же на компилируемость (в общепринятом понимании) + тащить за собой своё окружение всё же не идеал. Вижу по треду пару упоминаний непонимания Go. Представьте себе проект на платформе Ruby, NodeJs, PHP, Python, чаще всего состоящий из нескольких приложений на одной инфраструктуре. С ростом используемости нижележащие части кода стали хуже масштабироваться, появился смысл вычленять отдельные сервисы. Вот один из примеров. Это может случиться раньше, чем нужно, из-за некоторого хайпа вокруг микросервисов. Многое можно делать на основном ЯП, но почему бы не делать сразу с «запасом прочности»? Заодно с более дешевой поддержкой, т.к. standalone проще развернуть, закинув куда-то единственный инстанс и можно дольше обходиться этим одним инстансом без нужды в масштабировании.
Брать C#, Java только ради быстрой инфраструктуры мало смысла.


хороший кейс, спасибо что расписали. Выходит что Go может быть хорошим вариантом когда js/python уже не подходят, а java/c# тяжеловато.
вот тут только хочу спросить что вы имеете в виду под «быстрой инфраструктурой»? Быстрый запуск или быстрый доступ к массе инструментов из коробки в случае java/с#?
Возможно ли скажем такое что сначала микросервис кажется простым, берут Go, а дальше он растёт и оказывается что ему уже надо работать ну пусть там с AWS SQS а sdk нет, ну или там с Okta провайдером, и оказывается что инструменты либо плохи либо их нет, и выходит что изначальное предположение что всё окажется простым, не сработало и надо либо бороться с инструментом либо переписать на java/с#
вот тут только хочу спросить что вы имеете в виду под «быстрой инфраструктурой»?
Быстрая работа с сетью, прокси к другим сервисам с несложной логикой, просто микросервис, который используется активно несколькими приложениями (напр. event bus/sourcing), или банальная молотилка данных, например материализация или агрегация данных в БД. Если это делать на Go, то пройдёт намного больше времени до момента, когда оно начнёт захлёбываться и нужно будет придумать что-то лучше. При этом нужно меньше квалификации, чтобы сделать быстрый код.
скажем такое что сначала микросервис кажется простым, берут Go, а дальше он растёт и оказывается что ...
Вот как раз у Go, как ни посмотрю, в настоящий момент преимущество в поддержке библиотек и сервисов (наверно, не в последнюю очередь из-за Google).
Могу согласиться, что при росте кодовой базы будет быстро падать производительность разработчиков из-за невыразительности и слабой типизации Go. Субъективное мнение, но оно довольно распространено, по-моему. Наверно, учитывая невеликий обычно размер Go кода, лучше переписать на что-то более читаемое, чем продолжать бороться, или как-то изначально не выбирать этот ЯП для кандидатов на вырост.
Если это делать на Go, то пройдёт намного больше времени до момента, когда оно начнёт захлёбываться и нужно будет придумать что-то лучше.


я там выше писал что встречал довольно разные точки зрения на производительность Go. Кто-то говорит что у Go быстрый старт но потом выигрыша по сравнению с java нет. В этом есть некоторая логика, т.к. в java/с# сборщики мусора полируются годами, и была статья и тут на хабре что GC GO это не какой-то принципиально новый GC, а по сути типовый GC просто с другими настройками. Потому мне вполне понятно что в AWS Lambda Go имеет преимущество быстрого старта, но вот насчет постоянно работающего сервиса я не уверен.
Возможно я упускаю какой-то нюанс, конечно.
У вас скажем не бывало возможности сравнить производительность сервиса на java/с# и Go?

Могу согласиться, что при росте кодовой базы будет быстро падать производительность разработчиков из-за невыразительности и слабой типизации Go


года 2 назад я видел что у Go не было никакой возможности применять AOP (аспекты) и у меня сложилось впечатление что Go предоставляет только базовый уровень ввода-вывода в HTTP а дальше делай всё сам. Также я слышал что go-шники не любят фреймворки и говорят «если тебе нужен фреймворк — тебе не нужен Go». Т.е. мне кажется что пока это простая молотилка ивентов с S3 сделанная на AWS Lambda то всё ок, но как только нужно что-то чуть сложнее то на Go становится сложно.

Т.е. мне 2 года назад показалось что дело не столько в самом языке сколько в инструментарии. Может конечно и сам язык сделан так что не поддерживает никаких «премудростей». Сейчас ситуация как-то изменилась?
Возможно я упускаю какой-то нюанс, конечно.
Если мне понадобилось сделать вышеупомянутые вещи, то условно я беру VS Code, изучаю синтаксис в течение дня, делаю эту небольшой микросервис с простой поддержкой зелёной конкурентности в течение недели-нескольких и деплою как-нибудь результат компиляции.
А в Java/C#? Если совсем с нуля, используя как вспомогательный инструмент.

Да точно так же, мб быстрее потому что уже есть либы которые часть сложностей за вас спрятаны.

ну возможно у вас в .net всё получше, но если свитчер с python/nodejs станет переписывать на java + Spring то мне кажется переход будет непростой. Можно сказать что часто программист для интерпрайза на java это «Spring программист». Всё типа готовое, но когда надо кастомизировать то работает оно like a magic )

кстати вот вспомнил: Kubernetes и Docker тоже кажется на Go написаны
Почему такое пренебрежение к десктоп-софту? rustc вы бы тоже завернули в докер? Ну или VS, раз ею тоже пользуетесь на своей локальной машине

Да не пренебрежение, просто десктоп по сути мертв (<1% вакансий на хх на моем языке например), поэтому я по-умолчанию считаю что у человека не он пока тот не сказал обратное. rustc в докер я не завернул, а вот всякие эзотерики вроде идриса — так и стоят, черед докер. Ну и я выше написал: опенвпн, постгрес, монго — стоят через докер потому что это куда удобнее и быстрее делается.


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

<1% вакансий на хх на моем языке например

Это очень сильно зависит от языка: десктоп-вакансий на PHP будет ноль, на C++ — десятки процентов.

А конкретно портабельный десктоп софт это что-то либо очень старое и привет из нулевых, либо что-то очень редкое типа руфуса.

PuTTY, WinRAR, IrfanView, procexp, WinDbg, ImageMagick?
Инсталляторы у них есть, но пользоваться ими не обязательно.

Ни один из них бтв у меня не стоит, вроме виндбг который недавно пришлось поставить чтобы попробовать подебажить дамп (и к слову он не справился). так что да, это все редкие вещи, а windbg к слову через стор ставится, если мы про последние версии: https://www.microsoft.com/en-us/p/windbg-preview/9pgjgd53tn86?activetab=pivot:overviewtab


Винрар сменился севензипом, инфанвью потерял смысл когда браузеры научились все картинки показывать + в качествве редактора у людей всякие фотошопы/paint.net. В общем, не буду спорить, YMMV


Это очень сильно зависит от языка: десктоп-вакансий на PHP будет ноль, на C++ — десятки процентов.

ну я не по языкам смотрю, а в целом по вакансиям на любой яп. Исключая игродельство десктоп софта нового никто не пишет, максимум поддерживает старое. Исключения — единичные приложения вроде телеграма. А на один телеграм есть 1000 вакансий на очередной магазин

НЛО прилетело и опубликовало эту надпись здесь
А это для многих совсем не очевидная мысль. Люди могут предпочитать работать там где много денег, а не там где комфортно (тут все же часто присутствует дихотомия). Потом, вероятно, приходит выгорание…
Вовсе не разные, моя главная ценность — приносить максимальную пользу. Измеряется в моём случае коммерческой работы в долларах, прямо и непрямо заработанных компанией (вовсе не мной, кстати, у меня невысокая зарплата). Вот работал у нас как-то один, который всё хотел поскорее перевести на микросервисы, желательно на Go. Говнокодил, кстати, как не в себя, до сих пор легенды ходят. Вроде ушел на техлида куда-то, т.к. у нас ещё и плоская структура. Вот с ним не совпадают ценности, т.к. мне кажется он думал о своих интересах больше, чем об интересах компании. А с остальными наоборот совпадают в целом. Личные интересы (на что горят глаза) только могут не совпадать.
Сменить работу разработчику моего уровня квалификации весьма сложно, судя по опыту пассивного поиска (отвечать рекрутерам в линкедине). Если получится, то только на похожий или скорее даже более низкий технический уровень, судя по моему впечатлению о командах после редких интервью. Поэтому навряд ли что-то изменится, хотя может в активном поиске будет выше уровень интереса со стороны более продвинутых команд.
НЛО прилетело и опубликовало эту надпись здесь
Насколько я вижу, далеко не только для нашей команды характерны эксперименты дома по вечерам в области интересов, а на работе — «скучный энтерпрайз». Хотя удачные эксперименты из дома можно попытаться «продать» коллегам для работы, если это будет полезно для бизнеса. Напротив хорошо, если у всех разные интересы — больше новых идей для развития продукта.
НЛО прилетело и опубликовало эту надпись здесь
Именно так, разные интересы. Поэтому у нас на проекте анализируемые хинты, best practices на веб-фреймворке и в углу какой-то корреляционный анализ. Не только и не столько это, но разные интересы однозначно помогают проекту развиваться внутри. Поэтому не могу согласиться с
на этом уровне личные интересы таки должны совпадать
Уже говорил, повторюсь — если про язык периодически возникают обсуждения в духе «как наконец перестать ненавидеть его синтаксис и все остальное в нем», то значит синтаксис и правда плох, и оправдываться тут нечего (даже если кому-то он зашел). Выше 0xd34df00d упоминал Агду — вротмненоги же, да? Почему же там никто не воет? Да потому что там другая парадигма совершенно, отсюда другая публика, там это нормально. А вот Раст пытается пролезть туда, где люди пишут в основном на С/С++, и он на них критически не похож, несмотря на заверения. Так что этот пост и ему подобные недалеко ушли от анекдота про интимные отношения в семье сотрудника Майкрософт.
Си, когда появился, тоже был «критически непохож» на Фортран, Кобол и APL; но это ему не помешало.
Нет, обождите, я же выше специально указал, что есть разные ниши. И соваться в прочно стоящую на ногах нишу с совершенно новым синтаксисом — опасно (особенно когда и семантика поменялась совершенно). Вы привели в этот смысле нерелевантные примеры, Си вовсе не претендовал попасть туда, где «живут» АПЛ и Кобол. На самом деле и Фортрану он в конкуренты не набивался, хотя и по сей день им иногда приходится конкурировать в сфере высокопроизводительных вычислений. Си разрабатывался заведомо «ближе к аппаратуре», как язык системного программирования, он не мог составлять конкуренции Коболу так же, как Кобол не составлял конкуренции различным ассемблерам.

Лично для меня Rust закончился, когда я осознал, что он не умеет в UTF-8. Совсем. Инженеры просто не подумали, им по традиции наплевать на неанглоязычных пользователей.


Найти подстроку, вставить подстроку, заменить подстроку, разбить по подстроке — пиши всё сам, итератор по символам к твоим услугам.

Returns the byte index

Оперировать UTF-8 строкой как массивом уникодных символов невозможно в Rust. Ты можешь оперировать ей как набором указателей на место в буфере, где начинается какой символ.


В реальной жизни же уметь работать с UTF-8 строкой как с набором байтов не нужно вообще никогда.

Returns the byte index of the first character

Напомню, что в utf-8 строках номер символа бесполезен, потому что найти его в строке по номеру можно только последовательным сканированием строки. А вот номер первого байта как раз полезен.

Учитывая, что символ (grapheme cluster) в юникоде может состоять из нескольких codepoint'ов (в расте это char), работать с юникодной строкой как с массивом char'ов будет тоже неудобно.

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

Если разбить utf-8 строку по валидному byte index — то тоже получатся две валидные utf-8 строки.

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

Ну да, очень интересное: str[i..j].chars().count()


Остаётся только один вопрос — зачем?

Чтобы работать с utf-8 строками без лишних конвертаций, используя заложенные в utf-8 возможности. Очевидно же.

Ну да, очень интересное: str[i..j].chars().count()

thread 'main' panicked at 'byte index 1 is not a char boundary
Прекрасный способ отстрелить себе ногу.


Чтобы работать с utf-8 строками без лишних конвертаций, используя заложенные в utf-8 возможности. Очевидно же.

Да каких конвертаций? Что станет хуже, если str[i] указывает на char, а не byte?

thread 'main' panicked at 'byte index 1 is not a char boundary

Ну так передавайте туды валидные индексы, а не взятые с пололка числа!


Да каких конвертаций? Что станет хуже, если str[i] указывает на char, а не byte?

Оно и сейчас на char указывает, просто не любое значение i является валидным.

Если бы раст был завтиповой язык то он мог бы требовать:


slice : 
  (s : String) -> 
  (i : ValidPos s) -> 
  (j : ValidPos s) -> 
  GTE j i -> 
  String

Но увы, раст — это как раз мейнстримный язык "Без зауми", так что валидность индексов контролируйте сами, через, например, index_of и другие средства

Хуже станет то, что для обращения по str[i] каждый раз придется сканировать всю строку с начала.
Python решил эту проблему элегантно:
When it needs to create string objects, Python determines the highest code point in the string, and looks at the size of the encoding needed to store that code point as-is. Python then chooses that encoding — which will be one of latin-1, UCS-2, or UCS-4 — to store the string. All the gory details, if you want to read about them, are in PEP 393.
Для скриптового языка может оно и элегантно. Но для системного слишком далеко от zero-cost. Да еще и непрозрачна структура хранения для программиста.
В мире нет ни одной реальной задачи, в которой доступ к уникодной строке требуется побайтно.

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


В Расте банально вычислить количество уникодных символов между двумя байтовыми позициями — интересное такое занятие с неопределённым результатом.

Можете назвать сценарий, где такое может вообще понадобиться?


Вот знать, сколько байт занимает байтовое представление в строке — вещь нужная и полезная. А вот знать, сколько там кодпоинтов, символов или графем в подавляющем большинстве задач до лампочки.

Вот знать, сколько байт занимает байтовое представление в строке — вещь нужная и полезная. А вот знать, сколько там кодпоинтов, символов или графем в подавляющем большинстве задач до лампочки.

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

Я вас расстрою, но занимаемое место и размеры графических элементов совершенно никак не зависят от количества символов. Даже если шрифт моноширинный.

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

Ширина содержимого никак не зависит от количества символов. Она зависит от их графического представления.

Ширина содержимого никак не зависит от количества символов. Она зависит от их графического представления.

Даже если шрифт моноширинный.

Знаете, в юникоде и тексте вообще, столько нюансов и сложностей, что даже просто поменять регистр у символов — нетривиальная задача. Вы можете крутить эту тему в хвост и в гриву, но в реальных задачах используют много разных упрощений. Мы не о том, как делать правильно, мы о том, что КОЛИЧЕСТВО СИМВОЛОВ — ИСПОЛЬЗУЮТ.
НЛО прилетело и опубликовало эту надпись здесь

Скажите пожалуйста, а сколько вимволов в строке из вот этого вопроса? (в хабр увы не получается вставить): https://stackoverflow.com/questions/43618487/why-are-emoji-characters-like-treated-so-strangely-in-swift-strings

НЛО прилетело и опубликовало эту надпись здесь

В расте действительно есть какая-никакая поддержка юникода, но он иногда бывает слишком самонадеянным, пытаясь быть умнее там, где его не просили. Это приводит к вот такому:


    // concatenate, then lowercase
    let mut s = "".to_owned();
    s.push_str("AΣ");
    s.push_str("ΣA");
    println!("{}", s.to_lowercase());
    //⇒ aσσa

    // lowercase, then concatenate
    let mut ns = "".to_owned();
    ns.push_str(&"AΣ".to_lowercase());
    ns.push_str(&"ΣA".to_lowercase());
    println!("{}", ns);
    //⇒ aςσa

Сейчас мне, разумеется, скажут, что в реальной жизни так не бывает, а я отвечу, что я лично чинил такой баг (который, понятно, очень спорадический) в ситуации, когда название компании приклеивалось через дефис к неким метаданным для создания уникального идентификатора, а компания называлась — ну да, претенциозненько «ΜΕΣΣΊΑΣ».


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

Извините, а что изменилось бы если бы они вместо хардкода парсили спецификацию юникода, если хардкод этой самой спецификации соответствует?

Сейчас мне, разумеется, скажут, что в реальной жизни так не бывает, а я отвечу, что я лично чинил такой баг

И чем бы тут помог Эликсир? Да, операции перевода в нижний регистр и конкатенация не коммутируют, но это именно что логическая ошибка, язык бы тут не помог.

Эликсир-то тут вообще при чем?


В адекватных языках операции перевода в нижний регистр и конкатенация коммутируют. Спецификация консорциума напрямую говорит:


# Conditional Mappings
# The remainder of this file provides conditional casing data used to produce 
# full case mappings.

Язык общего назначения не должен был проигнорировать слово «conditional», но имеем мы ровно то, что имеем.


Ах, да, если вас вдруг интересует, чем бы помог эликсир: там конвертация концевой Σ в ς — вы не поверите, опциональна. Разработчик должен эксплицитно сказать: «используй тут правило для греческого». Да, так можно было.




mayorovp если хардкод этой самой спецификации соответствует

① Не соответствует, см. выше.
② Работало бы и завтра, когда выйдет новая версия спецификации, хардкор же, как обычно, поломается.

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


Выйдет новая версия спецификации — программисты напишут новый код для её поддержки, только и всего. Более того, в общем случае код всё равно придйтся писать всем, даже тем кто спеку парсит — ведь формат спеки может и поменяться.

новая версия спецификации какой-то магией подтянется во все реализации

Могу научить при компиляции скачивать актульную версию в каталог external_data, недорого. Ну, или посмотрите, как все Cldr имплементации это делают.


Выйдет новая версия спецификации — программисты напишут новый код для её поддержки, только и всего.

Конечно, ведь им надо за что-то платить зарплату — а тут, о ужас, — можно один раз написать нормальный код, и потом заниматься полезными делами. Я начинаю понимать, как даже при открытом сваггере какого-нибудь API, техотделы умудряются закладывать в план «переход на следующую версию».

Могу научить при компиляции скачивать актульную версию в каталог external_data, недорого. Ну, или посмотрите, как все Cldr имплементации это делают.

Для этого нужно компиляцию, как минимум, запустить, иначе магии не произойдёт.




Конечно, ведь им надо за что-то платить зарплату — а тут, о ужас, — можно один раз написать нормальный код, и потом заниматься полезными делами.

Ну вот и продемонстрируйте нормальный код, который корректно обрабатывает любые Conditional Mappings, включая потенциальные будущие, а мы посмеемся.

Проверил: в Java, в JS и в Python "ΑΣ" в нижний регистр переводится как "ας".
А где иначе? Только в эликсире?

В спецификации иначе. Этого недостаточно?

Ага, все вокруг неправильно спецификацию прочитали, один вы её прочитали правильно...

О, моё любимое «миллионы леммингов не могут ошибаться».


Можете. Чего вы не можете, так это сложить два и два, да подумать своей головой, вместо ссылок на авторитеты.

Найти подстроку, вставить подстроку, заменить подстроку, разбить по подстроке — пиши всё сам, итератор по символам к твоим услугам.

Поясните. В Rust же всё это есть.

НЛО прилетело и опубликовало эту надпись здесь

Полноценная работа с юникодом — вещь нереально сложная и, как мне кажется, должна быть реализована на уровне системных библиотек, а не на уровне языка и стандартной библиотеки.


А для остальных же действий, типа распарсить Json знание о юникоде и не нужно — мы просто оставляем строковые значения в их неизменном виде.

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

То есть вы считаете неправильным, что результат работы функций зависит от текущей локали?

Применительно к строковым функциям — багов из-за этого я вспомню больше, чем случаев, когда это было востребовано.
OpenNet токсичное сообщество, берём любой пост и смотрим первый пучок комментариев.
Так что пример комментария на мой взгляд неудачный.
В данном случае
int (*fns[16])(int * const * p);

можно использовать std::function, тогда объявление будет выглядеть так:
std::function<int(int* const* p)> fns[16];

Что, как мне кажется, даже более наглядно чем в Rust:
let fns: [fn(*const *mut isize) -> isize; 16];


Ну, и для этого тоже есть аналог, ничем не хуже
let a = foo();

auto a = foo;

Вот только std::function — это совсем не эквивалент указателю на функцию, а враппер с дополнительной индирекцией.

Извините я не очень хорошо знаю Rust, но в последнем случае разве приведённые сигнатуры ф-ий эквивалентны?

fn foo<'a, T: FromStr, I: IntoIterator<Item = T>, F: Fn(T) -> bool>(
    self: &'a Self,
    first: T,
    callable: F,
    iter: I,
) -> Result<&'a T, T::Err> {
    // ...
}


fn foo<T, I>(&self, first: T, callable: impl Fn(T) -> bool, iter: I) -> MyResult<T>
where
    T: FromStr,
    I: IntoIterator<Item = T>,
{
    // ...
}


Ведь в первом случае у нас стоит одинаковый квалификатор времени жизни для первого аргумента ф-ии, и первого параметра шаблона результата; а во втором случае квалификатора времени жизни нет.

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

Объясните пожалуйста кто знает.

Первая сигнатура означает, что возвращаемая ссылка не указывает на значение, возвращённое итератором. Видимо это ошибка. Должно быть self: &'a mut Self.


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


Во втором случае мы возвращаем значение, а не ссылку, так что таких ограничений нет.


Вообще-то такое впечатление, что кто-то писал эти сигнатуры не особо задумываясь об их смысле.


callable — обязательно поглощает переданное значение, так как нет ограничения T: Copy или T: Clone. То есть, если использовать callable с итератором, то максимум, что мы сможем узнать, — это сколько значений удовлетворяющих предикату callable вернул итератор, но какие это были значения мы узнать не сможем.

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


Но вопрос был про lifetime elision. Собственно, я ожидал подобного вопроса. WASD1 в примере не указано, как именно определен псевдоним MyResult:


type MyResult<'a, T> = Result<&'a T, <T as FromStr>::Err>;

Как видите, лайфтайм никуда не делся, просто в сигнатуре функции он будет выведен автоматически и поэтому указывать явно его не нужно. Если функция возвращает ссылочный тип, и при этом принимает в качестве аргумента только одну ссылку или если среди аргументов есть ссылка &self, то лайфтайм возвращаемого значения будет таким же, как у данного аргумента. Lifetime Elision

не указано, как именно определен псевдоним MyResult

Да, при таком определении лайфтайм будет выводиться и значения сигнатур будут в общем совпадать. Поэтому, чтобы не было такой неоднозначности, рекомендуют явно указывать присутствие лайфтайм-параметра у типа: -> MyResult<'_, T>


'_ в этом случае означает, что лайфтайм будет выведен с помощью lifetime elision.

Как мне кажется, намного проще и чище — просто не указывать. Для любого ссылочного типа лайфтайм может быть выведен, мы же не пишем из-за этого &'_ Type.

Если не писать, то теряется информация о владении. Foo<T> может быть владеющим (и его можно вернуть из функции foo<T>(_: T)), а может и не быть (и тогда его вернуть нельзя). В &T и так видно, что там нет владения, a чтобы показать отсутствие владения для типа Foo нужно явно писать Foo<'_, T>.

Публикации