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

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

в качестве черновика перевода, сгодится. если кратко: необходима вычитка.

Язык который не позволяет сделать простые вещи просто - тупиковая ветвь. Но это видимо не очевидно. Раст яркий пример когда взяв всего один приём пытаются им решить все вопросы, но при этом создают проблемы с которыми придётся иметь дело позже. И если "в системе типов есть всё" есть много желающих запихать туда еще больше. В результате распухание сложности гарантировано. При этом вообще-то никак не решена проблема разрастания зависимостей. Зато надо следить за большим количеством сущностей.

https://youtu.be/bKyxOaP-mDg?t=3120

Абстрактный наброс. Будут примеры?

этом создают проблемы с которыми придётся иметь дело позже

много желающих запихать туда еще больше.

При этом вообще-то никак не решена проблема разрастания зависимостей

Можно и не добавлять зависимости. Либо напишите, как надо решить, мы RFC-ку расту напишем.

Язык который не позволяет сделать простые вещи просто - тупиковая ветвь

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

Раст для меня всегда был как раз таким языком, когда можно простое сделать просто. Например вместо того чтобы городить полиморфизм через наследование и писать кучу абстракций, я пишу один простой enum со всеми нужными вариантами и данными и работаю с ним. Или вместо того чтобы создавать полноценный тип который делает какую-то полезную работу, просто пишу функцию. Вместо того чтобы сложно менеджить приватность полей, прописывать friend мемберы, просто обращаюсь к чему надо напрямую в рамках модуля. В расте нет наследования, что так же упрощает код и не позволяет нагромождать кучу абстракций ради абстракций - вместо этого используешь что-то по-проще вроде композиции. В расте нет разделения на struct/class (как в C#) или на типы значения/ссылочные типы - вместо этого есть просто тип. Нужно на хипе аллокнуть? Ок - создаёшь Box и готово. Хочешь статик полиморфизм? Юзаешь просто женерик и всё. Хочешь динамик? Создаёшь dyn и готово. Хочешь Option? Сделай, а не как обычно когда нужно наоборот следить чтобы null/None нигде не было. Опять же, упрощает код там, где None не нужен - убирает лишние проверки. В итоге как раз и получается что ты делаешь именно то, что нужно и минимально думаешь о второстепеных вещах

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

Я же привёл пример.

Нельзя в Расте просто взять и написать trace(myvar)

Т.е в продолжение темы про простые вещи просто, нужен ещё механизм макросов

Это ж библиотечный макрос...

macro_rules! dbg {

() => { ... };

($val:expr $(,)?) => { ... };

($($val:expr),+ $(,)?) => { ... };

}

Мда, просто. Да просто ппц

Судя по описанию ваших «понравилось» — вам нужен не раст, а хаскель.

Хаскель мне нравится, но не вижу причин отказываться от zero cost абстракций, возвращать GC и в целом отказываться от производительности, ограничивая диапазон применений языка

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

P.S.
За перевод спасибо :)

Неужели rust стоит того, программируя на нем, проходя через все эти препядствия? Переосмысливать все программирование и претерпевать эти страдания с новыми конценпциями? Мне кажется, что не cтоит.

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

Не только замена C/C++ но и Java/C#/Go

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

Порог входа для них высокий, ага

IDE + LLM в помощь!

Порог входа мне как раз высоким не показался. Мне показалось, что я не понимаю, какие именно нерешенные задачи решает этот язык. Даже у Го есть идея: горутины, хоть какой-то параллелизм из коробки, который к версии 2.0 даже может стать более-менее работоспособным. А зачем мне в мире, живущем по законам Мура раст?

какие именно нерешенные задачи решает этот язык.

Сочетание безопасности и скорости (языки с GC безопасные но медленные, околосишечные языки быстрые но небезопасные)

в мире, живущем по законам Мура

Во-первых, закон Мура вроде как несколько раз хоронили

Во-вторых, судя по этой фразе, именно из-за таких людей как вы появился закон Вирта)

языки с GC безопасные но медленные

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

околосишечные языки быстрые но небезопасные

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

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

И как вы собрались рубить бабло с бесплатной программы?

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

В чём смысл вопроса? unsafe для того и существует, чтобы предоставить программисту возможность реализовывать и самостоятельно проверять вещи, которые не способен проверить safe Rust, и изолировать эти вещи от остальной программы в этих самых unsafe-блоках (в околосишечных языках абсолютно весь код является unsafe, поэтому Rust тут безопаснее)

Ну какой вопрос такой ответ

Кроме управления памятью, полезные фичи Rust по сравнению с популярными языками (Java, C#, C++):

1. Безопасность многопоточности на этапе компиляции (отсутствие гонок данных).

2. Мощная система типов и алгебраические типы данных (Option, Result).

3. Встроенный pattern matching.

4. Макросы и метапрограммирование.

5. Отсутствие null (использование Option).

6. Выражения вместо инструкций (более лаконичный код).

7. Простая интеграция с C-библиотеками без накладных расходов.

8. Cargo — удобный встроенный менеджер пакетов и сборщик проектов.

9. Строгая модель владения и заимствования ресурсов (полезна не только для памяти, но и для любых ресурсов: файлы, сетевые соединения и т.д.).

🧮 Модель: #GPT-4.5 )))

То есть киллер фича это управление памятью и zero cost abstractions, но во многих областях он выигрывает по очкам

1. Безопасность многопоточности на этапе компиляции (отсутствие гонок данных).

Сами пробовали? Рекламные брошюры я перестал читать еще в школе. Вот простая и довольно частая задача: засосать CSV из файла с валидацией и группировками.

2. Мощная система типов и алгебраические типы данных (Option, Result).

Ух ты. А почему это хорошо?

3. Встроенный pattern matching.

О, да. Весьма среднего качества, правда. Но в 2025 это достижение, конечно, на фоне остальных гошечек.

4. Макросы и метапрограммирование.

Нет. Пока нет, по крайней мере. Метапрограммирование в расте в зачаточном состоянии.

5. Отсутствие null (использование Option).

О как. А почему это хорошо?

6. Выражения вместо инструкций (более лаконичный код).

Обалдеть! Выражения вместо инструкций! Вот бы Джону Маккарти и Стиву Расселу кто-нибудь это посоветовал в 1960 году!

7. Простая интеграция с C-библиотеками без накладных расходов.

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

8. Cargo — удобный встроенный менеджер пакетов и сборщик проектов.

Чуваки сделали пакетный менеджер? Не, ну это заявка, конечно.

9. Строгая модель владения и заимствования ресурсов (полезна не только для памяти, но и для любых ресурсов: файлы, сетевые соединения и т.д.).

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

  1. Пример неудачный) Эффекта параллельного парсинга не будет, остальное c# сделает лучше.

  2. Почему это хорошо:

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

Ошибки и некорректные состояния становятся явными и проверяются на этапе компиляции.

Код становится безопаснее, чище и понятнее.

Пример не буду приводить. C#, Java есть, но хуже. В Go нет ADT.

3. Согласен

4.Ok, я не в теме

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

6.В Rust почти всё — выражение, т.е. даёт значение, которое можно сразу присвоить или вернуть

let tax = if income > 50_000 { 0.20 } else { 0.15 };          // if-выражение
let size = match list.len() { 0 => "empty", 1..=3 => "few", _ => "many" }; // match-выражение
let n = { let a = 2; a * a };                                 // блок-выражение

Railway-pattern = цепочка «чистых» функций Result/Option → Result/Option.

В Rust каждое звено — обычная expression-функция

7. Интеграция с go проще чем на c#, java и примерно одинакова с Go. Python - C тоже удобно.

8.Cargo меньше конфликтов версий, воспроизводимость «из коробки» и встроенные механизмы защиты цепочки поставок. Отлично интегрирован в IDE rustrover. Лучше чем в с#

9.Идея заимствования ресурсов и отказ от GC позволяет например держать высокий и стабильный RPM без пауз с высокой P95 и P99

И добавлю

Хотите полноценные ADT + pattern matching и иные развитые ФП абстракции «из коробки» — смотрите на семейство ML и его наследников. Rust обгонит по популярности их всех. Хотя c# почти догнал по фичам и более популярен.

Это в OCaml и F#  — хороший паттерн-матчинг? Это просто смешно.

ADT мне лично нафиг не впёрлись.

А чего вам там не хватает?

Заточенных не на типы, а на данные матчеров и гардов.

case "foobar" do
  "foo" <> _ -> "FOO Family"
  _ -> "Dunno"
end

case [1, 2, 3] do
  [_, 2 | _tail] -> "Second is 2"
  _ -> "Dunno"
end

case 42 do
  i when is_integer(i) and i > 0 -> "Positive"
  _ -> "Dunno"
end

var = 42
case 42 do
  ^var -> 42
  _ -> "Dunno"
end

case %{foo: %{bar: 42}} do
  %{foo: %{bar: value}} -> value
  _ -> "Dunno"
end

case [1, 2, 3] do
  l when is_list(l) and length(l) == 3 -> :ok
  _ -> "Dunno"
end

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

И если вам не нужна проверка на полноту /исчерпываемость вариантов, то зачем?

Читаемость и деструктуризация по месту? И всё?

Хотя удобно для нетипизированных данных, где типы "определяются на лету".

Эти проверки всё равно выполняются рантайм

Это не совсем так. Проверка на полноту как раз есть, кстати.

Читаемость и деструктуризация по месту? И всё?

А это, типа, мало?

Мало для таких фанатских заявлений как выше. Ожидал больше

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

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

Ну вот конкретно с предпоследним проблемы. Ну и первый вариант некрасиво ложится. И ещё я не понимаю

var = 42
case 42 do
  ^var -> 42
  _ -> "Dunno"
end

а остальные есть — см ключевое слово when.

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

Первый пример точно нереализуем.

Первый пример точно нереализуем.

В таком виде, как есть, конечно нет. Но можно чутка подломать, и будет просто вызов String.starts_with:

match "foobar" with
| s when String.starts_with ~prefix:"foo" s -> "FOO Family"
| _ -> "Dunno"

То есть, менее мощно чем в типичных динамически типизированных языках, но обходится.

Это рантайм, а эрланг компилирует и оптимизирует матчи. Вот так, как выше — у нас как раз написать нельзя.

Гарды — твари времени компиляции.

А где у нас образцовый паттерн матчинг?

В эрланг pattern matching, конечно имеет богатые возможности, но не идеал.

Исчерпывающий match: Выражение match в Rust (аналог switch или case) требует исчерпываемости по умолчанию. В этом он даёт больше гарантий чем эрланг.

ADT мне лично нафиг не впёрлись

Потому что его Erlang / Elixir не умеет? Tagged tuples всё же не то.

требует исчерпываемости по умолчанию. В этом он даёт больше гарантий чем эрланг.

Нет, конечно. Если вы не знаете, что делать в случае неожиданного матча — надо падать, а не страдать.

Потому что его Erlang / Elixir не умеет?

Нет, не поэтому. Я использую Идрис там, где уместно. Я долго и много использовал языки с ADT. В результате пришел к выводу, что они мне не нужны в целом, кроме доказательств (см. Идрис). Раст так всё равно не умеет.

надо падать

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

явно прописать намерение упасть в коде, иначе о какой надёжности вообще может идти речь

Но вот ярлык «fault tolerant» почему-то приклеился именно к эрлангу, в котором принцип «Let It Crash» буквально выбран девизом. Неожиданно, правда, для людей с отсутствием банального кругозора?

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

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

Логично, unwrap должен предваряться unsafe

Во-первых, речь тут не про unwrap, а во-вторых, всё равно не должен, потому что падение не является небезопасным для памяти событием (самое страшное, что может случиться, это утечка, но Rust на данный момент считает утечки безопасными)

По памяти это событие вполне безопасное, согласен.

Программы на Расте безопасные, но могут течь и падать. Но делать это безопасно!

С этим я, очевидно, не согласен.

Если программист целенаправленно захотел заставить программу упасть явным написанием unwrap — это безопасно по всем параметрам, потому что всё работает в точности как задумано программистом и не имеет незапланированных побочных эффектов

Вы бы лучше вспомнили про паники, которые могут вываливаться в неожиданных местах — вот это реальная проблема Rust, на которую даже Линус ругался

А я и не в курсе пока про паники кроме некорректного анврапа, где?

Гуглится статья про Руст без паники, но это не то.

В любом случае такие места должны помечаться ансейфом например.

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

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

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

И проблема тут в том, что программист НИКАК не может узнать, будет ли тут выплёвываться паника или нет, и он никак не может запретить компиляцию кода с паниками, отчего у меня сильно пригорало, когда я пытался написать код без паник

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

Есть стандартный clippy, в котором есть возможность поставить запрет (написав в начале main.rs или lib.rs) как на получение элемента по индексу, так и по наличию в коде unwrap и разного прочего, что может вызвать панику:

#![deny(clippy::arithmetic_side_effects)]
#![deny(clippy::indexing_slicing)]
#![deny(clippy::string_slice)]
#![deny(clippy::panic)]
// ...

Потом проверить (в т.ч. вставив в CI):

cargo clippy

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

Тогда не понял претензию.
С одной стороны, Вы жалуетесь, что внезапно может возникнуть паника из-за того, что быстро написали vec[idx], а с другой — что есть писать не так, то «вынуждает перегружать код обработкой несуществующих ошибок».

Взаимоисключающие вводные…

Если у Вас настолько жёсткие требования по надёжности, то тогда Вам и код библитек надо тщательно проверять (например, на отсутствие закладок). В рамках проверки можно «натравить» на них clippy, благо их исходники доступны через crates.io (подавляющее большинство; бинарные сборки не считаем: по вводным надо тщательно проверять качество библиотек) и не брать недостаточно хорошо написанные (а насколько — поможет clippy, ему эти параметры можно командной строкой передавать).

Я хочу написать vec[idx] так, чтобы компилятор подтвердил, что я доказал корректность индекса и паники здесь быть не может, или чтобы компилятор сказал мне, что ничего не доказалось и паника возможна — ближе всего к этому подходит крейт no_panic, но он тоже кривой и имеет ложноположительные срабатывания (потому что работает через костыли)

Ну, я отвечал на реплику «и он никак не может запретить компиляцию кода с паниками». Запретить — может, хоть и с помощью утилиты [от разработчиков языка], которой предоставляет все необходимые данные.

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

P.S.: эта же утилита позволяет разработчику делать panic-и явными, что Вы ранее желали: «Если программист хочет падать, он должен явно прописать намерение упасть в коде». 😉

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

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

Ну блин, вместо того, чтобы узнать как надо делать [безопасно], только пополняю чёрный список недостатков Раст (

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

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

Railway-pattern = цепочка «чистых» функций Result/Option → Result/Option.

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

a = b?.c?.d слабовато, но ок

Не все языки даже такое умеют

Спорить с Gpt? Лол

Ответы гпт — это квинтэссенция человеческой глупости, просто по определению работы ллм.

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

Я внимательно читаю

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

А ваш понятийный аппарат я действительно понимаю с большим трудом. Но чья в том вина?

Но чья в том вина?

Если бы читал курс с кафедры — была бы моя. Если бы вам требовался выданный мной сертификат — была бы ваша.

А в комментариях на форуме — о какой вине вообще может идти речь?

Хорошо

Мы друг друга понимаем, хоть и не с одной итерации

А зачем мне в мире, живущем по законам Мура раст?

Исключительно «безопасное» ручное управление памятью. Предупреждая следующий вопрос древнекитайской философии «анахуаэто?» — я вижу только два применения:

  • Классические задачи ручного управления памятью — встройки, ответственное low latency и т.д.

  • Универсальные высококачественные библиотеки, которые используются в нескольких экосистемах (например, BLAS, libZ, какое-нибудь middleware, etc).

Ну вот, наконец, и разумный ответ на провокационный вопрос :)

Это всё так, но мир вокруг нас перестраивается с однополяр^W одноядерного на многоядерный. И раст мог бы бросить все усилия на примитивы параллелизации. А они вместо этого память в энумах выравнивают, выгрызая наносекунды на промахах кэша.

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

Согласен.

Раст после того как его по факту не взяли в Линукс мог бы сосредоточиться на параллельности и более продвинутом ФП (паттерн матчинг, завтипы, ...) и пойти в бизнес приложения

даже у Go есть идея: горутины

В других ЯП разве нет аналогов го-шных горутин?

хоть какой-то параллелизм из коробки

Звучит так, будто вы намекаете, что в пасте его нет

Звучит так, будто вы намекаете, что в расте его нет

Я не намекаю, я открытым текстом это заявляю.

Ну тогда разверните мысль, будьте добры)

Могу сказать, что это намного проще, чем учить С++ или пользоваться окаменевшим в мезозое С.

Я после 16 лет с JS/TS/Node перешел и мне нравится. Пишешь код, а половину тестов пишет за тебя компилятор. И предсказуемость выше, для финансового софта вообще прекрасно. И иммутабельные енумы со стейтом-объектом вообще прекрасны для написания кода. Я как-нибудь напишу статью про стейт-машины и Rust.

Напиши

Да, для стейт машины расто просто идеален.

Раст не позволяет и близко обеспечить гарантии конечного автомата без гигантского бойлерплейта и крайне неидиоматичного кода.

Есть гипотеза, что вы не очень понимаете, что такое стейтмашина.

Что такое, по-вашему, гарантии конечного автомата?

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

Нет, произвольный КА таких гарантий не даёт

Что такое, по-вашему, произвольный конечный автомат?

Вот и я хотел спросить ваше определение.

Сам по себе раст КА не яаляется, но реализовывать на нем будет удобно.

Что значит «моё»? Я пользуюсь хрестоматийным определением Майкла Сипсера из «Introduction to the Theory of Computation».

Традиционное определение конечного автомата
Традиционное определение конечного автомата

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

Напрямую.

Но ведь этих гарантий просто нет! Вот вам контрпример:

Q = { 1, 2 }
Σ = { a }
δ 1 a = 2
δ 2 a = 1
q0 = 1
F не важно

Переходы тут неидемпотентны по построению. Что там с гарантией?

Если бы вы удосужились прочитать мой предыдущий комментарий, вы бы (наверное) поняли, что речь про функции (δ 1) и (δ 2). Идемпотентность δ самой по себе приводит к вырожденным автоматам.

Но это не так критично, как гарантии защиты от непреднамеренных попыток взлома. Когда программист ошибся и руками установил стейт 2 в качестве реакции на вызов функции (δ 2).

К функции (δ 1) понятие идемпотентности неприменимо потому что у неё не совпадают области определения и значений. Рассуждать об идемпотентности можно только для функции (δ _ a), которая идемпотентной и не является.

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

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

ваши сообщения непонятны сколько их ни читай

Дык отпишитесь и не читайте, делов-то.

Ну вы сейчас занимаетесь демагогией по-современному, а хотелось бы подначить вас на занятия демагогией по-первоначальному!

В общем, у нас любой компьютер представляет собой конечный автомат, как известно (это если его не разбирать). Поэтому определение бессмысленно. А вот навыки работы с архитектурой на базе конечных автоматов — это давайте! В смысле, а не зафигачить ли вам статью на эту тему? Вот мы хотим делать архитектуру, вот где-то там конечные автоматы, вот гарантии и пр.

Ну и вообще, дался вам этот Матиас — мы вроде выяснили, что вот универсальные библиотеки (из-за несовместимости разных GC в разных экосистемах) и low latency/embedded — вот туда его. В остальных местах можно и нужно работать с GC.

Вот мы хотим делать архитектуру, вот где-то там конечные автоматы, вот гарантии и пр.

Запросто, у меня и пример из жизни прям есть.

Раст не позволяет и близко обеспечить гарантии конечного автомата без гигантского бойлерплейта и крайне неидиоматичного кода.

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

В принципе, КА реализуется как обычный switch. И требования к языку там минимальные, отслеживание зависимостей в Расте, и удобный паттерн матчинг и обработка ошибок по моему даст удобства для реализации КА.

Какие требования выставляете Вы, что этого мало?

Без примера непонятно.

Классический турникет. 2 состояния (без потери общности). Вы получаете в состоянии закрыто — ивент «закрыть». Как это обработается свичом?

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

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

Непротиворечивость машины в целом, ЯП не потянет, забота программиста.

забота программиста

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

Где-то я уже читал про турникет)

Класс!

почему именно программисты на JS всегда говорят про "половину тестов за вас пишет компилятор"? А, точно, они привыкли писать на динамически типизированном языке


Ну тогда зайдите в С++ и там внезапно тоже "компилятор пишет тесты за вас"

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

Никак не пойму, зачем сделано так неудобно.

1 Пример 1й. fn longest(.

1.1 Почему компилятор, отлично умеющий высчитывать лайфтаймы, не может здесь сделать автовывод лайфайма?

1.2 функция чистая, зачем вообще в ней вычисляется владение?

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

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

В данном конкретном случае без понятия, но dangling pointer все ещё является большим источником проблем. Механизм lifetime это в том числе попытка его полностью исключить.

Из другого треда удалится?

Не может, владение не даст.

Погодите. Но как раз таки даст. Если вы описываете владение таким образом, то чем это отличается от GC?

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

Нет нагрузки в рантйме

Проблема не в самой функции, а в лайфтайме результата.

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

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

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

Ещё поговаривают если у вас код обрастает лайфтаймами, что компилятор не может вывести сам - скорее всего это явный сигнал, что вы делаете что-то не то

Потому что правила автовывода лайтаймов (lifetime elision) строго определены, и данный случай под эти правила не подходит.

Вот пример кода, где выставление одинакового лайфтайма (времени жизни, ВЖ) во всех местах в функции foo_incorrect не пройдет проверку БЧ (borrow checker):

// https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=1ce9cb09c241bf12fba2bd33574b24b0

fn main() {
    let mut str_mut = "";
    
    let s1 = String::from("str1");
    {
        let s2 = String::from("str2");
        str_mut = foo_incorrect(&s1, &s2); // fail here
        // str_mut = foo_correct(&s1, &s2); // will be ok
    }
    
    println!("{str_mut}");
}

fn foo_correct<'long, 'short>(a: &'long str, b: &'short str) ->  &'long str {
    todo!()
}

fn foo_incorrect<'a>(a: &'a str, b: &'a str) ->  &'a str {
    todo!()
}

Надо понимать, что БЧ выполняет проверку ВЖ для каждой функции отдельно, не заглядывая в тела других использующихся функций. То есть, в примере выше - неважно что будет написано в теле функций foo_correct \ foo_incorrect - проверка ВЖ для функции main от этого никак не изменится: учитываться будут только сигнатуры вызываемых функций.

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

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

Пример 2 my_func(s); my_func(s);

2. Почему переменная сгорает (потребляется). Ну передали владение параметру (опять же константное), но функция отработала и Вернула долг!

В итоге я не могу написать просто так my_func(s); Trace(s);

И зачем?

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

А если не уверены то просто копируйте

fn longest_owned(x: &str, y: &str) -> String {
    if x.len() > y.len() { x.to_string() } else { y.to_string() }
}

Вот именно, должен и сам это понимать. Без лишних вопросов

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

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

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

Лямбды тоже можно таскать глобально по всей программе
struct LambdaHolder {
    func: Box<dyn Fn(i32) -> i32>,
}

fn not_lambda(a: i32) -> i32 {
    a + 2
}

fn give_me_your_lambda(l: &mut LambdaHolder, b: i32) {
    l.func = Box::new(move |a| { a + b });
}

fn main() {
    let mut l = LambdaHolder {
        func: Box::new(not_lambda),
    };
    println!("{}", &(l.func)(2));  // 4
    give_me_your_lambda(&mut l, 5);
    println!("{}", &(l.func)(2));  // 7
}

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

Проблемы с сигнатурами (если был автовывод и не учтен в манглинге ) могут возникнуть если в разных модулях будет разный неучтенный лайфтайм.

Ну теоретически всё ещё можно пробросить pub struct LambdaHolder или pub fn give_me_your_lambda в стороннюю библиотеку (но писать ещё один громоздкий пример я не буду)

А делать две разных логики для публичных и приватных функций — по-моему только ещё больше всех запутает

Да, вероятно с помощью dyn, можно будет сломать безопасность. Захват по ссылке лямбдой как источник проблем, где то это уже было =)

но функция отработала и Вернула долг!

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

И зачем?

Чтобы пользователь этой функции мог полагаться на явно объявленные гарантии, а не что-то там гадать про наличие или отсутствие долгов

Гарантия уже есть, описана сигнатурой. Иммутабельная переменная, иммутабельное содержимое.

Параметр передаётся не по ссылке.

Параметр передаётся не по ссылке.

Именно! А это означает, что функция получает полное владение параметром и, если захочет, может уничтожить его. А значит из этой гарантии следует, что использовать параметр после вызова функции нельзя (иначе рискуем получить use-after-free).

Есть ломающий пример? Как функция с иммутабельным параметром может его уничтожить

Так он есть прямо в посте:

fn my_func(v: String) {
    // делаем что-то с v
}

После вызова my_func(s) владельцем параметра становится функция. Когда работа функции завершается, у параметра больше не остаётся владельца. Раз владельца больше нет — объект автоматически уничтожается и освобождается связанная с ним память. Этот факт защищает от утечек памяти, а невозможность использовать параметр повторно — от use-after-free

Я понимаю, что владение передаётся. И потом уничтожается.

Я не понимаю, зачем оно передаётся (в случае иммутабельного параметра для начала) ? Это неудобно

Это вопрос не к реализации владения, а к дизайну языка

зачем оно передаётся

Потому что программист так захотел и именно так объявил в сигнатуре функции

Как функция с иммутабельным параметром

(в случае иммутабельного параметра для начала)

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

fn my_func(v: String) {
    let mut v = v;
    v.push_str("мутирую азаза");
}

Наличие или отсутствие мутабельности у значений — это больше защита от случайных опечаток, чтобы программист случайно не сделал то что изначально не планировал (но если всё же запланировал, то он может явно объявить мутабельность как в примере выше)

А вот мутабельность в ссылках это будет уже совсем другой разговор

Т.е отбрасывание иммутабельности даже не требует unsafe o_O

Не поверил, полез на плейграунд.

Тогда логика есть. Перректальная правда

Ну, это не создаёт никаких уязвимостей, связанных с повреждением памяти, так что нет технических причин объявлять это unsafe ¯\_(ツ)_/¯

Вообще то создаёт. Пример очень простой, есть сегменты данных, защищенные от записи (тупо ROM, 4ex) , как сегменты кода. Переменная может оказаться в таком сегменте, ибо вызов функции изменения, судя по сигнатуре не предполагает.

Переменная может оказаться в таком сегменте

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

Строки, которые прописываются в строковых литералах (let s = "Hello, World!";) и предположительно будут помещены компилятором в сегмент данных, имеют другой тип &str — то есть ссылка на кусок строки без мутабельности (а точнее &'static str, что означает, что эта ссылка остаётся валидной в течение всей жизни программы)

Ну предположительно, это тонкий лед =)

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

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

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

Вообще то создаёт. Пример очень простой, есть сегменты данных, защищенные от записи (тупо ROM, 4ex) , как сегменты кода.

Не может локальная переменная оказаться в сегменте ROM (иначе как через особо хитрую глобальную оптимизацию с анализом всей программы - но такая оптимизация и присваивание let mut v = v; увидит).

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

Это принцип работы borrow checker. Если владение будет передаваться неявно, то для компилятора может стать проблемой вычислять какой области видимости принадлежит переменная особенно в async. Если хочется добиться логики которую вы описали, то следует явно передавать ссылки на значения:

fn main() {
    let mut c = "str".to_string();
    f1(&mut c);
    f2(c);
}


fn f1(c: &mut String) {}
fn f2(c: String) {}

Нет, я не хочу мутабельности.

Я спрашиваю, почему вдруг потребляется иммутабельный Параметр?

Если это небезопасно, то нужен пример, в каком случае.

@andreymal привел отличные примеры. Также могут быть вызваны memory leaks. Если наша функция аллоцирует память, но раз мы не освобождаем s, то логично, что не должны освобождать и другие переменные. А чекать, что s пришла извне и ее надо сохранить, а остальные не следует, может быть довольно затратно особенно когда мы подключаем async, пойнтеры и ТД. Плюс мы не сможем так просто параллелить компиляцию и как будто в таком случае проще воспользоваться GC-языками. Ну и хочется явно видеть, что функция делает.

Очень невнятный поток сознания.

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

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

Это мы уже обсудили.

Нет только понятия причин такого дизайна. Возможно, где то на родном сайте есть мнение от создателей.

Может, потому что делать иначе просто нет смысла? Для немутабельной передачи с сохранением жизни исходной переменной уже есть передача по ссылке. А владение с запретом мутабельности для меня звучит как-то противоречиво, это уже как будто и не владение совсем

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

Например, часто встречается interiour mutability (доступный через примитв UnsafeCell), который позволяет менять объект, если есть какая-то гарантия, что никто больше его не меняет. Например, Mutex берет блокировку, а Cell запрещает передавать себя между потоками.

Поэтому осмысленнее иметь не мутабельные/иммутабельные ссылки, а уникальные/неуникальные. Тогда получается три самых распространенных способа передавать объекты (если не считать Pin'ы):

  • владение (`T`) — объект принадлежит тебе полностью и исключительно; никаких ссылок на него не существует и теперь это твоя ответственность освободить память.

  • Shared-ссылка (`&T`) — объект где-то есть, и твоя ссылка на него не уникальна; возможно его кто-то может читать из других потоков.

  • Unique-ссылка (`&mut T`) — объект где-то есть, но кроме тебя его никто не читает и это единственная активная ссылка на него. Часто это позволяет менять объект.

Тогда общая картина становится немного более логичной. Владение позволяет брать на объект ссылки (в рамках borrow checker'a) и делает тебя ответственным за освобождение ресурсов. А вот уже ссылки позволяют получать доступ к объекту. Так у нас владение и ссылки становятся ортогональными концептами с непересекающимися фичами.

Из факта владения объектом часто следует возможность взять &mut ссылку (или обычную), но не всегда borrow checker это разрешит. Например, если вы передали &Vec<T> в функцию и получили в ответ ссылку на один его элемент, то теперь вы не можете взять и добавить элемент в вектор, хотя им владеете и несёте ответственность освободить память. Ведь вдруг при вставке вектор переаллоцируется и указатель сломается. Borrow checker не даст создать уникальную ссылку на Vec, пока существует какая-то другая ссылка на него же. Таким образом, владение объектом не гарантирует возможность доступа к нему; чтобы взаимодействовать с данными нужно создавать соответствующие ссылки.

А мутабельность owned значений (`let mut x: T`) просто синтаксическая соль, которая на самом деле ни на что не влияет в большом смысле.

В принципе логично - иммутабельность == константность это синтаксический сахар и полагаться на неё нельзя.

А заимствование объекта функцией - это как дать в долг соседу бухарику, надежды на него нет =)

А владение - преходяще - сейчас есть, через пять минут потерял.

Так я скоро постигну Дзен Раста. \о/

Потребляется он потому, что программист так написал. Чтобы не потреблялся, надо передавать по ссылке. Кроме мутабельной ссылки есть ещё и иммутабельная:

fn f3(c: &String) {}

Почему переменная сгорает (потребляется).

Потому что так написана функция my_func. Написали бы её принимающей &str - ничего бы не сгорало.

Вот пример из C++
my_func(std::move(s)); my_func(std::move(s));
Тут так же можно задать вопрос: почему переменная сгорает?
В C++ кстати, это просто скорее всего приведёт к багу, так как мы будем использовать объект после его мува. Раст же честно скажет: тут что-то не так с кодом, может тебе нужно значение скопировать/склонировать?

Что касается копирования, да, в C++ можно написать так my_func(s); my_func(s); и это будет работать. Только этот код будет неявно копировать весь s. А что если s это какой-то сложный объект? Например строка, что аллоцируется в куче? Тогда придётся всю строку переаллоцировать - это вносит в код нетривиальные и неожиданные расходы. В расте это поведение просто явное: хочешь расходы на копирование строки? Хорошо, напиши это явно:
my_func(s.clone()); my_func(s);

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

Только этот код будет неявно копировать весь s.

Необязательно, смотря как объявлена my_func(). Может не стоит сравнивать с языком, который не знаешь?

Короче, мув семантика по умолчанию неудобна.

Как объявить my_func(s) чтобы s всегда автоматически мувалась?

Второй пример в этой статье

Хорошо. Какая там сигнатура у my_func будет?

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

Покажите как в Расте делается иерархия вложенных виджетов, которые шарят общие стили и связываются ссылками между собой и с моделью/контроллером. Как декларации времени жизни предотвращает утечки памяти. Как borrow checker обеспечивает безопасность памяти.

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

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

Простите, а где Вы увидели «из примеров и новостей», что «Раст-программы так же текут и падают на сложных задачах»?

Приведите, пожалуйста, конкретные ссылки.

Пожалуйста:

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

Говорить про «утечки и краши» языка, и привести как аргумент ссылку на форум, где обсуждается «unwrap() hurts readability» …

Понятно.

По приведённым ссылкам:

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

  • Вторая ссылка — это вопрос в Reddit, почему у его приложения течёт память, в «UPDATE» он написал, что сам это устроил («The issue was me creating…»).

  • Третья ссылка — про unwrap. Не очень понятно, зачем он в этом списке. Если программист явно говорит «тут нужно упасть», то странно предъявлять языку программирования, что «оно тут падает».

  • Четвёртая ссылка — про unsafe Rust. Тоже не очень понятно, зачем приведена. Если Вы выходите (на что должны быть очень веские причины) из safe Rust на территорию, где Вам надо тщательно следить за тем, что происходит, то странно предъявлять языку программирования, что он не делает проверки там, где не должен. В статье явно это написано в conclusion: «unsafe Rust is an indispensable tool when it is necessary to bypass the strict safety rules».

По сути:

Написать говнокод (который течёт и падает) можно на любом языке, было бы желание. Ни один язык программирования не защитит от бесконечной вставки чего-либо в массив, от чего будет «утечка памяти» или от явного указания программистом «упади здесь».

Когда Вы заявляете, что «Раст-программы так же текут и падают на сложных задачах», это воспринимается, как то, что почти любая более-менее сложная программа будет «так же» (то есть с той же вероятностью и частотой) течь и/или падать. Это не так. В Rust-е (видимо, надо отдельно оговорить, что в Safe Rust, что покрывает бо́льшую часть кода, который пишут) гораздо проще сделать приложение, которое НЕ будет течь или падать в неожиданном месте.

Что касается защиты от Data Races — об этом любая статья про Cell или Mutex или Atomic: точно определяются те структуры, к которым может идти конкурентное обращение в runtime, и без явного захвата владения (или явного одалживания, предотвразщающее захват владения) невозможно получить доступ к этим данным. Всё остальное контролируется компилятором (в safe Rust) — либо владение (или единственная ссылка — &mut) с возможностью изменения, либо одалживание (borrow) с возможностью только чтения.

Почему все статьи нахваливающие Раст статьи никогда не приводят реальных примеров

Потому что реальные примеры — в реальных программах (см. open source). В статьях — иллюстрации на небольших элементарных примерах, чтобы не перегружать внимание читающих.

видимо, надо отдельно оговорить, что в Safe Rust, что покрывает бо́льшую часть кода, который пишут)

Исследования (например, RustBelt) показывают что ~30–50% крейтов используют unsafe хотя бы один раз. Более 90% кода Rust косвенно зависят от библиотек, которые используют unsafe, поэтому зависимость от них транзитивно вносит unsafe. Safe Rust существует только в академических примерах.

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

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

Тестовый набор JDK содержит десятки тысяч тестов, в общей сложности более двух миллионов строк кода. Это относительная гарантия того, что unsafe Java runtime не будит ломать safe Java код.

Если я подлючу случаный крейт с unsafe-фрагментом кода, где гарантия, что он был написан столь же ответственно, и что его ежедневные фиксы не сломают мое приложение?

Возможно вы не знаете, но стандартная библиотека Rust тоже покрыта десятками тысяч тестов.

Если я подлючу случаный крейт с unsafe-фрагментом кода, где гарантия, что он был написан столь же ответственно, и что его ежедневные фиксы не сломают мое приложение?

А если я подключу случайную библиотеку в Java, где гарантия, что она не вызовет внутри гонку данных? Или правильно вызовет С библиотеку, не приводя к UB.

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

Просто в Rust такие блоки кода намеренно выделены в блоки unsafe, чтобы быть заметнее. А в каком-нибудь Python вызов C-API легко затеряется среди тысяч строк кода.

Более 90% кода Rust

100% кода Rust зависят от библиотек, которые используют unsafe, потому что стандартная библиотека

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

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

~30–50% крейтов используют unsafe хотя бы один раз.

Какие претензии к языку? Не используйте crate-ы, в которых есть unsafe, если его так сильно опасаетесь.

Код unsafe легко найти, отсмотреть и понять, насколько он [там] нужен, насколько корректно обрабатываются инварианты и надо ли [по этой причине] доверять этому crate-у.

поэтому зависимость от них транзитивно вносит unsafe.

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

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

Rust как язык позволяет относительно просто делать программы без большого количества уязвимостей (memory access, race condition и пр.) и багов за счёт определённых гарантий безопасности по сравнению с другими языками. Он вроде бы никогда не заявлялся как Абсолютно Безопасный Для Всего.


¹ https://ru.wikipedia.org/wiki/Предпочтение_нулевого_риска

Что касается защиты от Data Races 

Как в такой схеме предотвращаются дедлоки?

Никак, потому что deadlock не имеет прямого отношения к Race condition. Deadlock может случиться, когда используются блокировки для предотвращения Race condition, но, строго говоря, deadlock можно устроить при обычном межпроцессном взаимодействии вне зависимости от языка программирования (и безо всяких Mutex-ов).

С тем же успехом можно спросить: «как Rust защищает от непреднамеренного удаления таблицы в БД?»…

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

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

Да правда что ли?

Deadlock — это ошибка уровня дизайна системы, которая [ошибка] может быть внепроцессная (межпроцессная). Неважно, каким именно образом будет реализовано ожидание освобождения ресурса: хоть бесконечный периодический опрос (polling) Redis-а.

Как язык программирования может проверить (предотвратить) что-то на межпроцессном уровне?

никто не дает обычным приложениям прав

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

Речь идет о дедлоках в одном приложении, в многопотоке, не в IPC.

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

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

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

Хорош ли Rust как язык тем, что он позволяет программистам сосредоточиться на высокоуровневой логике, освобождая программистов от необходимости тщательно следить за всем кодом на относительно low-level уровне, но предоставляя достаточно большие гарантии и проверки в compile-time, в т.ч. memory safety, preventing race condition (наиболее часто встречающиеся уязвимости) и достаточно удобную систему типов с zero-cost abstraction (высокий performance без stop the world из-за GC — просто как бонус)? По-моему — да, и я с радостью его использую.

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

Переполнение стека и накопление аллокаций в теоретически достижимых точках иерархии объектов - это частные следствия проблемы останова (halting problem). Они не имеют решения, поэтому все системы, гарантирующие отсутствие утечек памяти (и вообще любых ресурсов) явно или неявно исключают из рассмотрения эти недоказуемые "утечки".

Проблема Раста (среди прочих) в том, что он совершенно никак не болется с утечками памяти, в то время как его промоутеры или не акцентируют внимание публики на том что "Раст течет" либо вовсе раздают гарантии что Раст "prevents most leaks", ведь если прямо цитировать РастБук (memory leaks are memory safe), то народ начинает задаваться вопросом, а так ли велика выгода за те ограничения, которые вводятся Растом?

а так ли велика выгода за те ограничения, которые вводятся Растом?

После того, как мне неоднократно приходилось дебажить утечки в сишечных программах, ответственно заявляю — да, достаточно велика, «prevents most leaks» всё ещё намного лучше чем «prevents no leaks»

memory leak это safe в расте

Но из этого же никак не следует, что программы на Rust текут просто по умолчанию.

Запомните уже наконец: memory safety - это не про гарантии отсутствия «утечек», это про гарантии отсутствия UB при некорректной работе с той самой памятью, которое легко допустимо в С/C++, например. И (safe) rust дает эти самые гарантии.

Почему то в Википедии и много ещё где про мемори сайфети написано по другому. Не надо приплетать свои фантазии.

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

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

Википедия, глава Memory safety абзац Contributing bugs.

В растономиконе не упоминается, потому что раст от переполнения стека и утечки памяти не защищает.

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

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

К мемори сэфети в Расте у меня претензий нет, даже не уверен что есть ЯП с лучшими гарантиями

Википедия никогда не являлась авторитетным источником нигде, а уж в CS — и подавно.

Какие предлагаются более авторитетные источники определения?

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

Поэтому в контексте «memory safety in Rust» имеет смысл ссылаться на: Unsafety
и, ради смеха, на Nomicon.

Зачем вы мне даёте какие-то ссылки? Я ответил на ваш вопрос, а не задавал свой.

Чтобы не повторяться

Я не очень понимаю, что вы хотите донести.

Есть некий язык, чуваки говорят: вот такие гарантии мы даём, вот это мы называем unsafe. Они в своём праве навести аксиоматику и её использовать.

«А у меня другое определение» — работает (со скрипом) только в глобальных дискуссиях, внутри разговора о расте имеет смысл пользоваться их определениями.

Да всего лишь спор о терминологии. Чем мемори сайфети в Расте отлично от определения глобального.

И зачастую в рекламных целях, одно другим подменяется. Те руст заявляется как мемори саейф на 100%, понимая сокращённый список ошибок этого класса.

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

Это Вам очевидно, а очевидно, что для @alex88django_novice не очевидно, и он не единичный случай =)

Я где-то писал, что раст гарантирует memory safety в понимании термина с Википедии? Не приплетайте свои фантазии)

Ну и, чтобы не быть голословным, предоставьте, пожалуйста, действительно авторитетный источник, где заявляется, что memory leaks относятся к классу ошибок memory safety, а то «много где еще пишут» - такой себе аргумент :)

предоставьте, пожалуйста, действительно авторитетный источник, где заявляется, что memory leaks относятся к классу ошибок memory safety

Пожалуйста: https://www.merriam-webster.com/dictionary/memory

Погуглить за тебя классические учебники по специальности программирование? Извини

Совсем тролли обленились в наши дни :)

Да у вас там unwrap везде в этом safe коде. А неудачный unwrap для клиента ну ничем не отличается от SIGSEGV или неперехваченного исключения.

Если вы хотите безопасно обработать возвращенный вам Result / Option, вы можете использовать паттерн-матчинг или специальные методы этих типов. unwrap() - небезопасный способ.

И где вообще (и кем) заявлялось, что раст гарантирует отсутствие thread panics ?

И где вообще (и кем) заявлялось, что раст гарантирует отсутствие thread panics ?

Конечно, никем. Просто все тут вокруг орут, что «безопасно». :-)

Если вы хотите безопасно обработать возвращенный вам Result / Option, вы можете использовать паттерн-матчинг или специальные методы этих типов.

Спасибо, я знаю. Кстати, называть такой действительно безопасный метод словами unwrap_or — это довольно конкретное отсутствие понимания, что такое unwrap... Ну и вообще отсутствие чувства языка.

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

Да у вас там unwrap везде в этом safe коде. А неудачный unwrap для клиента ну ничем не отличается от SIGSEGV или неперехваченного исключения.

Вы так пишете, как будто любая проблема с памятью приводит самое худшее к SIGSEGV. Но это ж не так...

Для очень большого числа программ это и есть самое худшее.

Нет. Самое худшее — это далеко не SIGSEGV. Если он произошёл — то просто невероятное везение.

Самое худшее — это незаметная порча содержимого памяти, где расположены другие важные (для приложения и пользователя) данные или remote execution, когда приложение начнёт выполнять что-то, что нужно злоумышленнику (находящемуся далеко от самого приложения), а не пользователю.

Самое худшее — это незаметная порча содержимого памяти

Где-как. В ряде мест ну произошла порча и ладно.

Спасибо за ссылку на Makerpad

  • Этот проект использует опасные техники, приводящие в крэшам, такие как unwrap и unsafe код.

  • Его трекер содержит десятки багов с крэшами.

  • Его пустой пример hello world занимает 126.4M RAM

  • 99% этого проекта написано не на Расте, а на внутреннем DSL, который вводится макросом live_design!. Этот отдельный язык дополнительно затрудняет написание, отладку, сопровождение этого проекта и всех проектов, которые его используют, поднимают порог входа в и без того непростой Раст. Сам факт наличия DSL для UI говорит не в пользу выразительности Раста.

Внутри Makerpad для хранения графа сцены используется полностью самописная библиотека умных указателей (WidgetRef), которая никак не связана с Растом. Этому может быть только два правдоподобных объяснения:

  • система управления памятью Раста не пригодна для безопасной и эффективной работы с древовидным DOM-ом

  • или она столь сложна, что проще написать свою собственную.

Я просто вспомнил, что буквально на днях в рассылке прилетела именно эта ссылка и ответил на «Покажите как в Расте делается иерархия вложенных виджетов».

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

«Покажите как в Расте делается иерархия вложенных виджетов».

GUI — это сразу же ООП или, хотя бы, сборщик мусора. Это вам любой скажет, кто натрахался с С++ными GUI библиотеками.

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

Какой?

GUI — это сразу же ООП или, хотя бы, сборщик мусора.

Вообще то не обязательно. Но Ооп местами тут удобно.

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

на методике владения обходится проблема сложности ручного освобождения памяти

Что-то вот я совсем не уверен.

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

Поэтому даже как-то странно, что на Русте не могут сделать это всё.

Только пока вы не перетащили кнопку из панели А в панель Б.

А их разве таскают? Ну сделали клон, а старую удалили нафиг.

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

Ну да, но каких-то ужасных проблем вроде не видно. Другое дело, что ну совершенно непонятно, зачем всё это, весь этот borrow-checker, все эти попытки сделать что-то очень быстрым, когда требования «не сделать ничего слишком медленным», да и подчистить за собой рано или поздно.

сделали клон

Звучит не очень круто для языка, стремящегося быть blazingly fast )

А зачем вам blazingly fast в GUI? Интерфейсы — это hard realtime, отклик 120 FPS, а не скорость пакетного исполнения.

Открепили от одного родителя, прикрепили к другому. Не вижу сложностей. Это даже в Delphi работало.

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

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

Это как-то обусловлено или это в чистом виде косяк исполнения?

А почему косяк-то? Наоборот же, удобно.

Я не очень понимаю, зачем...

И пример ниже меня не убеждает.

Ну вот смотрите, вы создали некоторый элемент управления, кнопку там или текстовое поле. У вас лежит ссылка на эту кнопку в приватном поле. Вы через это поле меняете кнопке текст, а также включаете-выключаете её. Вы обрабатываете у кнопки события.

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

С какого вообще перепугу время жизни кнопки должно быть привязано к какому-то там родителю? Ради увеличения числа багов?

Удобнее всего если это сделаете вы - в таком случае и висячих ссылок не оставить за собой проще

Мне кажется, как раз, если родитель. Родитель — это окно, в котором это есть.

А события — ну если нет адресата, ну не должно оно ронять ничего. Просто ну не получится у вас послать сообщение, и славно.

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

А события — ну если нет адресата, ну не должно оно ронять ничего. Просто ну не получится у вас послать сообщение, и славно.

Тут уже языковое ограничение, в Delphi 7 события не так работают.

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

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

Создаешь новое специальное окно со своим набором элементов управления

Ну всё — новое главное окно со своей независимой иерархией. Т.о. у нас виджеты формируют не дерево, а лес.

data AppWidgets = [Widget]

data Widget = Widget ... ~children:[Widgets]

вместо

data AppWidgets = Widget
data Widget = Widget ... ~children:[Widgets]

Вы не много не понимаете, как устроен Delphi. В Delphi, в VCL, например, окно - это отдельный пользовательский класс на базе пустого окна. Т.е. для каждого окна принято (так делается автоматически) описывать класс. Каждое поле которого - контрол. Вернее, каждый контрол окна - это поле класса, другие поля тоже можешь конечно же описывать. И все эти контролы - сплошным списком. При этом, владелец контролов только один - окно. Даже если контролы там вложены друг в друга.

Дерево строится именно связями контролов через Parent. И его ты видишь только в рантайм (или в дизайнере). В коде все контролы доступны напрямую через класс окна.

Т.е. даже если у нас кнопка внутри поля ввода, доступ к ней доступен через Self.Button1, или Form1.Button1, а не Self.Edit1.Button1.

Parent здесь - это визуальная составляющая, позволяющая менять место, где контрол находится визуально, не меняя ответственного за контрол. Ведь ссылка на контрол все ещё находится в классе, где он был описан.

Я не писал на дельфи почти 30 лет, но даже тогда у меня была собственная библиотека компонентов, которую я таскал из проекта в проект, и они все имели свои вложенные компоненты, доступ к которым из окна осуществлялся как раз Self.MyComp1.MySubComp1.

Владелец контрола тот, кто его объявил (инстанциировал).

Именно, тот кто инстанциировал. Если создавать окно в дизайнере, все контролы - поля класса окна. Оно их владелец. А родитель - визуальный аспект, говорящий где оно рисуется.

Естественно, ты можешь делать отдельный класс поля ввода и внутри него создавать для него другие контролы. И тогда будет доступ только через само поле. Но и в этом случае, ты можешь поменять Parent кнопки внутри поля ввода и владелец опять остаётся поле ввода. Ведь именно оно его инстанциировало

Спасибо. Я так понял, что они споткнулись на написании компилятора(ов).

Там и так непросто, а тут вам вставляют палки в колёса этим borrow checker'ом. Посмотрите, даже в clang'е есть свой сборщик мусора.

Можете посоветовать книжку, в идеале от создателей языка, в которой описывается, какие задачи хотели решить и почему сделано вот так вот, а не иначе? Этакий аналог K&R.

От создателей языка это в первую очередь «The Rust Programming Language» (это название книги) и «The Rustonomicon» — вообще они предназначены для обучения написанию программ на Rust, но ответы на некоторые вопросы «почему» в них тоже имеются

Растономикон не устарел? Я его пробовал читать еще до версии языка 1, когда он часто менялся.

Предположу, что это не так важно, особенно с учётом того, что стабильная версия 1 уже есть и фундаментальные принципы вряд ли поменяются в обозримом будущем (из существенных изменений разве что async/await и Pin, но про это можно отдельно почитать в какой-нибудь книжке про асинхронщину)

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации