TL;DR:
— в Rust намного больше достоинств, чем просто скорость и безопасность;
— в Rust по умолчанию CDD (compiler-driven development, разработка через компилирование). Это как TDD, только CDD;
— Rust — не сложный язык, особенно если не гнаться за максимальной производительностью.
На Rust можно смотреть с разных сторон. Например, можно на него смотреть как на безопасную замену для C или C++. Многие говорят, что ниша Rust — это "mission critical" программы, а все, кто использует его для других целей — безумцы (цитата из одного из многочисленных Rust vs. Golang тредов). При этом среди людей, которые используют Rust, распространено мнение, что memory safety в Rust — это не главное его достоинство (например, тут, тут, тут или тут).
В этой статье я бы хотел рассказать:
— почему взгляд на Rust как на "memory safe C" очень сильно сужает область его возможного применения;
— почему я смотрю на Rust как на очень удобный в разработке язык высокого уровня, которому просто случайно повезло оказаться невероятно быстрым;
— почему разработка на Rust быстрее, чем многие думают;
— почему Rust — это один из лучших языков общего назначения.
Это только мое мнение, я не профессиональный Rust разработчик, если вы с чем-то не согласны, то пишите комментарии, обсудим.
Upd: не успел я эту статью выложить, как оказалось, что 27 марта 2024 года на конференции Rust Nation UK 2024 было выступление с интригующим названием Beyond Safety and Speed: How Rust Fuels Team Productivity от Lars Bergstrom, Google Android Director of Engineering. В этом выступлении есть примерно половина тем из этой статьи, и чтобы в каждом пункте не писать «И в выступлении Ларс говорит то же самое!», я упомяну это выступление один раз, когда буду говорить о скорости разработки (т.к. это - основная тема этого выступления и этой статьи). Рекомендую посмотреть его полностью, видео всего 30 минут, ссылка с таймкодом. Видимо, это не только мое мнение.
Часть 1. Вступление
Еще со школы меня интересовал C, было интересно разобраться, как же оно там под капотом работает. У меня до сих пор где-то лежит книга «Язык программирования С», но я её так и не дочитал до конца, не говоря уже о том, чтобы «выучить» C. Как ни странно, но «виноваты» в этом холивары на хабре. Ведь из них я узнал, что C — это жонглирование работающими бензопилами на минном поле или что-то подобное.
Даже после того, как я уже работал программистом и был уверен в своих силах, я опасался C, боялся того, что если когда-нибудь напишу что-то сложнее hello world
то там через строчку будет UB, use after free, segfault, buffer overflow и т.д. и т.п. C и сам по себе очень помогает такому ощущению. Как люди без страха пишут на языке, где функцией вывода в stdout из стандартной библиотеки надо пользоваться осторожно, т.к. с помощью неё можно читать и изменять произвольную память. Для меня сейчас у C/C++ есть только 1 ниша: легаси С/С++ код, который слишком долго и дорого переписывать на Rust, и меня такая ниша не привлекает.
С первых прочитанных статей Rust меня заинтересовал. Он как C, только при этом безопасный! Что же может быть лучше возможности покопаться в железе напрямую без UB? Как оказалось потом, бывает еще лучше.
Часть 2. Rust. Начало
После Python писать на Rust было тяжело. Везде ссылки, ничего не компилируется, наработанные годами подходы не работают. К счастью, меня это не остановило, я продолжал читать книжки, смотреть разнообразные ютуб-лекции и туториалы и прочие материалы. Лучший из них — курс лекций Алексея "matklad" Кладова (не уверен, что @matklad — это он, если это ты, отзовись) для Computer Science Center. В них вместо того, чтобы через 7 часов лекций объяснять, какой синтаксис у циклов, на первой лекции есть вот такое:
После такого понимаешь, что все будет серьезно
#![no_main]
#[link_section=".text"]
#[no_mangle]
pub static main: [u32; 9] = [
3237986353,
3355442993,
120950088,
822083584,
252621522,
1699267333,
745499756,
1919899424,
169960556,
];
Или мой любимый слайд — «Модель Памяти C++ за Один Слайд» (слайд 35).
Этот курс меня сразу заинтересовал, с тех пор я пересмотрел его, наверное, раз пять и все еще нахожу новые вещи, на которые раньше не обращал внимания. В какой-то момент я узнал, что в качестве базы для домашних заданий используется статья Ray Tracing in One Weekend, и я тоже решил попробовать написать свой трассировщик лучей. Это уже не hello world и не переписывание примеров из книг, это достаточно большой проект, чтобы оценить, как язык ведет себя в реальной жизни. Это был первый момент, в который я понял, что Rust это не только «memory safe C».
Часть 3. Fearless concurrency
Из вышеупомянутых лекций я уже знал, что в safe Rust невозможны гонки данных и что писать многопоточный код в Rust намного проще. Но то были просто слова, их надо было проверить в деле. В конце "Ray Tracing in One Weekend" есть список возможных доработок, и одна из них — это параллелизм. Под конец упражнения мне хотелось отрендерить красивую 1080p картинку, но даже при 100 лучах на пиксель это занимало достаточно времени для того, чтобы меня это не устраивало. И звезды сошлись: 12-ти ядерный процессор, медленный однопоточный рендерер и Rust c бесстрашной конкурентностью.
По началу не особо получалось, мьютексы помогли коду скомпилироваться, но они блокировались так, что код оставался по сути однопоточным. Долго я бился в многопоточность, ничего не получалось. Но все же у меня получился вот такой код:
Заголовок спойлера
let rows = img.rows_mut().collect::<Vec<_>>();
std::thread::scope(|scope| {
...
for (y, chunk) in rows.into_iter().enumerate() {
scope.spawn(move || {
...
for (x, pixel) in chunk.enumerate() {
let mut color = Color::default();
for _ in 0..samples_per_pixel {
...
}
*pixel = color.as_rgb(samples_per_pixel);
}
...
});
}
});
img.save("image.png").unwrap();
Просто потоки. Просто параллельная обработка пикселей. Ни одного мьютекса или какого-либо еще примитива синхронизации. И оно заработало. И заработало в 10 раз быстрее. Но суть даже не в этом, главное в том, что несмотря на то, что у меня было (да и сейчас) не много опыта в многопоточных приложениях, но я за пару часов смог прикрутить многопоточность без блокировок.
Кажется, что в этом коде нет ничего сложного. Мы просто создаем потоки, в которых по выполняем части большой задачи. Но у этого кода есть такие гарантии, которые не могут дать многие другие языки:
в этом коде нет гонок данных;
невозможно написать эту программу так, чтобы получить пересекающиеся задачи;
невозможно написать эту программу так, чтобы в момент
img.save
хоть один из потоков был бы еще жив.
Что еще важно, в этом Rust нет встроенных в язык механизмов для таких гарантий для многопоточного кода. Гонок данных и пересекающихся задач нет из-за borrow checker'а (нельзя иметь больше оной уникальной (мутабельной) ссылки на объект), синхронизация ганантирована областью видимости замыкания в std::thread::scope
.
Upd:
На самом деле все было не совсем так
В изначальной версии был другой пример и, так получилось, что вместо "из-за гарантий компилятора в Rust гораздо проще писать конкурентный код" получилось скорее "смотрите, в Rust есть библиотека для создания пула потоков" (даже с учетом .par_bridge().into_par_iter()
) что, конечно, очень странный повод для гордости. Насколько я помню, когда я писал этот код в 2020 году в стандартной библиотеке scoped потоков еще не было.
И это добавило еще один пример к тому, что Rust невероятно просто рефакторить. Оказалось, что я не сохранял Cargo.lock
и из-за этого все библиотеки обновились, в том числе и ломая обратную совместимость. Мне потребовалось 15 минут для того, чтобы:
починить все, что сломалось из-за новых версий библиотек;
написать новый многопоточный код, который лучше иллюстрирует то, что я хотел передать;
проверить, что картинка правильно генерируется.
За 15 минут я склеил 2 разных версии программы, которая использует многопоточность так, что код заработал. Какой еще язык способен на такое я не знаю.
Изначальная версия
По началу не особо получалось, мьютексы помогли коду скомпилироваться, но они блокировались так, что код оставался по сути однопоточным. Долго я бился в многопоточность, ничего не получалось пока я не вспомнил про библиотеку rayon, которая делает многопоточность проще. Я долго не мог разобраться, как же правильно и идиоматично её применять и у меня получилось сделать только такое:
Заголовок спойлера
...
use rayon::ThreadPoolBuilder;
use num_cpus;
...
fn main() {
...
let samples_per_pixel = 100;
let pool = ThreadPoolBuilder::new()
.num_threads(num_cpus::get())
.build()
.unwrap();
let mut img = RgbImage::new(image_width, image_height);
...
let mut pixels = img.enumerate_pixels_mut().collect::<Vec<_>>();
pool.scope(|scope| {
…
for chunk in pixels.chunks_mut(image_width as usize) {
scope.spawn(move |_| {
for (x, y, pixel) in chunk {
...
for _ in 0..samples_per_pixel {
...
}
**pixel = color.as_rgb(samples_per_pixel);
}
...
});
}
});
img.save("image.png").unwrap();
}
Просто пул потоков. Просто параллельная обработка пикселей. Ни одного мьютекса или какого-либо еще примитива синхронизации. И оно заработало. И заработало в 10 раз быстрее. Но суть даже не в этом, главное в том, что несмотря на то, что у меня было (да и сейчас) не много опыта в многопоточных приложениях, но я за пару часов смог прикрутить многопоточность без блокировок.
Тут можно сказать, что это тривиальная задача, у нас есть список задач, есть пул воркеров и есть пачка вариантов как безопасно и быстро скармливать задачи воркерам. И я соглашусь, это действительно тривиальная задача, но в ней можно сделать достаточно большое количество ошибок. Вместо дебага гонок данных или дедлоков, код просто не компилировался с подробными ошибками о том, почему я дурак и, иногда, как это исправить. Конечно, компилятор отловил не все ошибки: в начале я подумал, что в pixels.chunks_mut
надо передать сколько в конце должно быть чанков и очень удивился, когда у меня программа создала несколько десятков тысяч потоков. Но это было легко заметить и исправить.
Еще можно вспомнить опыт Mozilla. Они несколько раз пытались сделать их CSS движок многопоточным, но у них ни разу это не получилось. А на Rust они смогли это сделать. Так что это работает не только на маленьких пет-проектах, но и на огромных монстрах вроде браузеров.
Кстати, потом я воспользовался библиотекой rayon и получилось еще более понятно (но не так интересно, насколько я помню, где-то внутри там есть мьютекс):
Заголовок спойлера
...
use rayon::prelude::*;
...
let mut img = RgbImage::new(image_width, image_height);
...
img.enumerate_rows_mut().par_bridge().into_par_iter().for_each(
|(_, chunk)| {
...
chunk.for_each(
|(x, y, pixel)| {
...
for _ in 0..samples_per_pixel {
...
}
*pixel = color.as_rgb(samples_per_pixel);
});
...
}
);
img.save("image.png").unwrap();
Вся многопоточность свелась к .par_bridge().into_par_iter()
. Этот код настолько же быстро работает, но его гораздо проще писать и гораздо проще понять, что происходит.
И это самый главный плюс Rust (имеется в виду safe Rust) - в нем тяжело написать программу так, чтобы она компилировалась, работала неправильно, но как-то не очевидно. Если код скомпилировался и получается ожидаемый результат, то в подавляющем большинстве случаев код делает именно то, что от него хочется (и обычно не только в позитивном случае).
Ошибки, которые Rust не находит
Больше всего времени я потратил на баг, где написал -
вместо +
. Из результата вычислялся квадратный корень, который с этим багом иногда возвращал NaN
. NaN
в свою очередь заражал все, чего он коснется, и в конце концов на финальном рендере это были небольшие группы черных пикселей в неожиданных местах. К сожалению, о signaling NaN
я узнал только после того, как нашел причину и поправил код.
Увы, не думаю, что когда-либо будет существовать такая система типов (или какой-то другой механизм), который мог бы предотвратить подобные ошибки. Логические ошибки останутся с нами до тех пор, пока будет программирование. Но все же возможность избавиться от огромного числа ошибок, оставив только логические, сама по себе стоит того.
Часть 4. Обработка ошибок
Кстати, о логических ошибках. В Rust лучшая система обработки ошибок.
Паники
Паники именно к обработке ошибок я не отношу, перехватывать их стоит в достаточно небольшом количестве случаев, и если расценивать их как «вместо паники тут была бы дыра в безопасности», то, мне кажется, с ними все в порядке.
Подход Rust не уникальный, монады сами по себе существуют давно, и языки, которые их используют, тоже. Их слабая распространенность иногда не дает мне заснуть в 3 утра. Я могу понять, почему в старых языках, например C, нет монад. Как бы я ни относился к C, все же ему больше 50 уже, не те времена были.
Лирическое отступление или почему Golang вызывает у меня только разочарование
Я недоумеваю, почему в относительно новых языках их нет? Я не могу без смеха (а иногда слез) читать эту цитату:
This is cleaner, even compared to the use of a closure, and also makes the actual sequence of writes being done easier to see on the page. There is no clutter anymore. Programming with error values (and interfaces) has made the code nicer. ... In fact, this pattern appears often in the standard library
Заголовок спойлера
type errWriter struct {
w io.Writer
err error
}
func (ew *errWriter) write(buf []byte) {
if ew.err != nil {
return
}
_, ew.err = ew.w.Write(buf)
}
ew := &errWriter{w: fd}
ew.write(p0[a:b])
ew.write(p1[c:d])
ew.write(p2[e:f])
// and so on
if ew.err != nil {
return ew.err
}
Тяжело не согласиться, все правда. Только проблема в том, что это монада. Буквально, это монада error, только хуже сразу в нескольких местах:
она самописная, её надо для всего реализовывать самостоятельно;
она не позволяет группировать разные операции. Если бы надо было не сделать N записей, а прочитать файл, создать другой и записать что-то в третий, то код такой обертки сразу станет сильно сложнее;
можно спокойно проигнорировать ошибку, и компилятор (подозреваю, и линтеры) по этому поводу даже не пискнут.
Может быть, раз уж "this pattern appears often in the standard library», стоило бы его в отдельную абстракцию вынести? Или это бы сделало Golang слишком "brilliant"? Последняя часть относится к известной цитате Роба Пайка:
The key point here is our programmers are Googlers, they’re not researchers. They’re typically, fairly young, fresh out of school, probably learned Java, maybe learned C or C++, probably learned Python. They’re not capable of understanding a brilliant language but we want to use them to build good software. So, the language that we give them has to be easy for them to understand and easy to adopt
Сколько я ни видел эту цитату, я никак не могу понять часть про "they’re not researchers". Не нужно быть "researcher", чтобы пользоваться языком, но желательно, чтобы те, кто язык придумывал, были как раз ими, чтобы потом простым людям жилось лучше и не приходилось код копипастить.
Это было небольшое превью возможной статьи «Почему Golang вызывает у меня только разочарование», если вам вдруг интересно было бы почитать, то пишите в комментариях. С одной стороны, мне больно от мыслей об упущенных возможностях в языке, а с другой, — уже давно хочется поделиться со светом этой болью, а то чего я один страдаю.
В Rust обработка ошибок крутая: если наловчиться, она позволяет писать код так, как будто этих ошибок вообще нет, но при этом они никуда не исчезают и всегда проверяются. В качестве примера приведу вот такой код:
Заголовок спойлера
let installed_apps: HashSet<String> = fs::read_dir(steam_library)?
.map_ok(|entry| entry.path())
.filter_ok(|path| path.is_file())
.filter_map_ok(|file_name| {
let f = file_name
.file_name()?
.to_str()?
.strip_prefix("appmanifest_")?
.strip_suffix(".acf")?
.to_string();
Some(f)
})
.collect::<Result<_, _>>()?;
Это часть простого скрипта, который удаляет ярлыки уже удаленных с компьютера игр из стим. Ни скорости, ни низкоуровневого доступа тут вот вообще не нужно, но я все равно написал его на Rust, т.к. хотел попрактиковаться в идиоматичной обработке ошибок. Мне кажется, получилось неплохо. Да, количество разных методов поражает воображение, часть из них еще и не из стандартной библиотеки, но при этом этот код учитывает:
что
fs::read_dir
может вернуть ошибку;что объекты, которые возвращает итератор по результату
fs::read_dir
, могут быть ошибкой;что
path.file_name()
возвращаетOption
, т.к.path
— совсем не обязательно путь именно к файлу;что
file_name.to_str
возвращаетOption
, т.к. путь к файлу не обязан быть UTF-8;что
.strip_prefix
возвращаетOption
, т.к. строка может не начинаться наprefix
;что
.strip_suffix
возвращаетOption
, т.к. строка может не заканчиваться наsuffix
.
Скажу сразу: не всегда получается вот так хорошо, бывает и что-то более... кхм... в общем, сами смотрите:
Осторожно, 38+! Беременным детям не смотреть!
let mut buffer = String::new();
let _ = fs::read_dir(shortcuts)?
.map_ok(|entry| entry.path())
.filter_ok(|path| {
path.extension()
.and_then(|ext| ext.to_str())
.and_then(|ext| ext.strip_suffix("url"))
.is_none()
})
.map_ok(|p| -> anyhow::Result<Option<PathBuf>> {
buffer.clear();
let mut file = fs::File::open(&p)?;
file.read_to_string(&mut buffer)?;
let app_id = buffer
.find(template)
.and_then(|url_position| buffer.get((url_position + template.len())..))
.map(|app_id_start| {
app_id_start
.chars()
.take_while(char::is_ascii_digit)
.collect::<String>()
});
let Some(id) = app_id else {
return anyhow::Result::Ok(None)
};
if installed_apps.contains(&id) {
return anyhow::Result::Ok(None)
};
anyhow::Result::Ok(Some(p))
})
.flatten()
.filter_map_ok(|path| path)
.map_ok(|p| trash::delete(p))
.flatten()
.collect::<anyhow::Result<(), _>>()?;
Если честно, я сам с трудом понимаю, что тут вообще происходит (особенно без подстановки типов из IDE). Возможно есть вариант, как это сделать более читаемо, но, когда я писал этот код, он мне в голову не пришел. Не уверен, что этот код работает правильно (я проверил, что он компилируется). Я его даже не запускал т.к. сразу понятно, что это не то, что хочется. Переписать его в более императивном стиле получилось заметно лучше:
Заголовок спойлера
let mut buffer = String::new();
for entry in fs::read_dir(shortcuts)? {
let path = entry?.path();
if path
.extension()
.and_then(|ext| ext.to_str())
.and_then(|ext| ext.strip_suffix("url"))
.is_none()
{ continue }
let mut file = fs::File::open(&path)?;
buffer.clear();
file.read_to_string(&mut buffer)?;
let Some(app_id) = buffer
.find(template)
.and_then(|url_position| buffer.get((url_position + template.len())..))
.map(|app_id_start| {
app_id_start
.chars()
.take_while(char::is_ascii_digit)
.collect::<String>()
}) else { continue };
if !installed_apps.contains(&app_id) {
trash::delete(path)?;
}
}
В этом случае код стало гораздо легче читать, но если присмотреться, то в изначальном варианте много проблем вызывало то, что я очень хотел написать оптимальный код и переиспользовать 1 буфер для чтения всех строк. Без этого все было бы гораздо проще. Это абсолютно не играло никакой роли для этого приложения, но хотелось сделать быстрее просто потому, что можно. Об этом явлении мы поговорим немного позже, а дальше у нас...
Часть 5. Скорость разработки
Это все, конечно, замечательно, но у нас 152 релиза в день, надо фичи пилить, нам некогда воевать с компилятором. Да, наш код регулярно ломается от NullPointerException (или его вариации в других языках), от неправильной обработки некритичных ошибок, от гонок данных, от куста проблем, которые связанных с памятью и т.д., но зато мы делаем это быстро! Пока вы там ошибки компилятора исправляете, мы уже на рынок выйдем и захватим его!
И вот теперь мы подошли к тому, что стало поводом для написания этой статьи. Подобные мысли у меня уже давно, и я регулярно рассуждаю на подобные темы с друзьями и коллегами, но вдохновение на эту статью мне дала одна недавняя череда событий, о которой чуть-чуть позже. Но для начала нам надо поговорить про Python.
Вообще я python бэкенд разработчик. Python как язык мне скорее нравится, чем не нравится, но у него точно есть очевидные проблемы, которые нет смысла отрицать (хотя понемногу их исправляют, что дает надежду на будущее). Не могу сказать, что делал какую-то ракетную хирургию, в основном обычные «получи json, сохрани json в базе, преобразуй json, переправь json дальше».
Мне приходилось дебажить все это очень много раз. Когда несмотря на 100% покрытие типами, в sentry появляется AttributeError: 'NoneType' object has no attribute '...'
, когда по какой-то причине половина бизнес-процесса для какого-то запроса не применилась и пытаешься по логам понять: это клиент врет о том, что он на самом деле сделал или это какая-то редкая бага из-за того, что оказалось, что ТЗ двух разных фич, которые были сделаны в разное время и разными людьми, противоречат друг другу, и это произошло первый раз за год работы сервиса. Много веселых (и не очень) историй у меня есть про дебаг и рефакторинг Python кода.
Нет повести печальнее на свете, чем повесть о питоне и рефакторинге. Рефакторить код в Python очень трудно, типизация и тесты делают этот процесс немного проще, но все равно огромное количество времени тратится на «Да что ж за тип у этой хрени такой», «А это-то тут откуда появилось?», «Откуда еще это исключение выползло» и т.д. и т.п.
Личный опыт рефакторинга Python кода
Я хочу поделиться двумя историями о рефакторинге Python кода, в одной из которых я был просто наблюдателем, а другую же делал сам.
История первая:
Как-то я работал в месте, где спокойно функционировал и развивался один сервис. Все было с ним хорошо, он работал, приносил деньги компании, даже фичи новые в него добавляли. Но была у него проблема. Это был сервис на Python 2.7. А поддержка Python 2.7 закончилась. И сервис практически без тестов (и естественно, без типов). И еще надо зависимости обновить до актуальных. И по-хорошему — код отрефакторить под новые зависимости. И тесты написать. К счастью, все это прошло мимо меня, но я видел, насколько долгий и мучительный был этот процесс.
История вторая:
Наши сервисы использовали внутренний протокол (ничего такого, просто стандартизированный XML, стандартизированные URLs). Он использовался во многих сервисах, но в каждом его приходилось писать заново (или копипастить из другого сервиса). Нам это положение вещей не нравилось, и, когда пришлось писать новый сервис, мы решили заодно вынести этот протокол в отдельную библиотеку. Само по себе это было дело несложное, но я решил потратить время на то, чтобы пользоваться библиотекой было удобно.
Немного контекста
У нас были свои внутренние библиотеки, но некоторыми из них было либо тяжело пользоваться, например для работы одной из них нужно было скопипастить пару сотен строк кода из другого сервиса (а сначала понять, какой код специфичен для сервиса, а какой нужен всем). Мне хотелось этого избежать, и я потратил примерно неделю на тестирование различных вариантов, перед тем как определился. В конце концов, мне кажется, у меня получилось выполнить эту задачу, и для работы (хотя и самой простой, вместо 404 ответить 200) хватало 10 строк кода.
И казалось бы, все хорошо: роутинг, парсинг и многое другое инкапсулировано в библиотеке, она отлично работает в новом сервисе, никаких проблем. Но теперь надо было переводить старые сервисы на эту библиотеку. И несмотря на то, что библиотека была типизирована, и в сервисах были тесты заменяемой логики, рефакторинг все равно занимал очень много времени. Я руками проверял каждый возможный путь, дописывал недостающие по покрытию кода тесты, но все равно несколько раз находил новые ошибки уже после того, как думал, что закончил. То объект не тот передается, и у него нет нужного метода, то импорта не хватает, то еще что-нибудь.
Я бы с радостью рассказал о рефакторинге Golang, но, к моему счастью, мне никогда не приходилось этого делать. Наверно, это немного лучше, чем Python, но тот факт, что если при добавлении нового поля в структуру забыть её проинициализировать, и компилятор даже warning не покажет и просто подставит zero value, говорит само за себя (наверняка на этот счет есть линтер, но такой код просто не должен компилироваться. А про очевидность поведения zero value для разных типов мне даже говорить не хочется).
В отличие от Python (и многих других языков) Rust дает подходящие инструменты для рефакторинга кода, например система типов, обработка ошибок и перечисления, требующие обработки всех вариантов. Эти инструменты помогают чинить, дополнять или полностью переписывать код гораздо быстрее и с меньшими усилиями. В Rust рефакторинг намного проще, и из-за этого на нем можно разрабатывать не медленнее, а иногда даже быстрее, чем во многих других языках, которые «проще» и для «быстрой разработки».
Опыт рефакторинга Rust кода
Вернемся к моему трассировщику лучей. Через несколько лет, как я его написал, мне в голову вдруг пришла идея использовать для цвета не f64 (double), а u8 (unsigned char), который при этом будет валиден в любой момент работы программы (в оригинальной статье цвета за каждую итерацию складывались и потом делились на samples_per_pixel
, а мне хотелось поддерживать актуальный цвет).
Но проблема! Код написан под f64, никаких дженериков там нет (немного есть, например вот такой ужас. Это не то, как надо писать дженерики в Rust, мне просто было интересно, как далеко можно зайти. Как оказалось, достаточно далеко). И я начал рефакторинг. Просто фиксил код, пока компилятор не перестал выдавать ошибки. И код заработал. Без тестов, без ничего. С первого раза после того, как код скомпилировался, он заработал.
Заголовок спойлера
На самом деле я немного преувеличиваю: получилось очень смешно, и в первый раз заработал только... красный канал. Как я потом выяснил, пока я проверял ошибки от компилятора, захардкодил нули в зеленый и синий каналы, и они никак не менялись. Вместо нулей надо было просто использовать todo!(). Для компилятора проблем бы не было, все типы сходятся, но при этом, если код запустить, он запаникует, и будет сразу понятно, что и почему не работает.
Надеюсь, вы простите мне это художественное преувеличение, не думаю, что оно хоть как-то повлияло на суть. И кстати, если кто-то дочитал до этого момента и думал, что я эксперт в Rust, то это не так. Я просто иногда пишу на нем разные вещи для развлечения и смотрю умные лекции на ютубе в надежде стать умнее, не более того.
И вот теперь, спустя я уж не знаю сколько времени, мы добрались до того повода, который и сподвиг меня написать эту статью.
Я решил разобраться в том, как работает Алгоритм Ахо — Корасик. Даже нашел вроде бы неплохую статью, сижу, пишу. Дошел до части с функциями go
и get_link
и врезался в бетонную стену компилятора. Половина строк функций помечена как ошибки. Все выглядит очень невесело. Сижу я и думаю: да, тяжело на Rust писать всякие деревья, связные списки, рекурсивные обходы и прочее подобное. Вот примерно тот код, который у меня получился:
Заголовок спойлера
fn go(&mut self, curr_node_idx: usize, ch: char) -> usize {
let curr_node = &mut self.nodes[curr_node_idx];
if let Entry::Vacant(all_links) = curr_node.all_links.entry(ch) {
if let Entry::Occupied(next) = curr_node.next.entry(ch) {
all_links.insert(*next.get());
} else {
let new_link = if curr_node_idx == 0 {
0
} else {
let next_link = self.get_link(curr_node.parent);
self.go(next_link, ch)
};
all_links.insert(new_link);
}
}
curr_node.all_links[&ch]
}
fn get_link(&mut self, curr_node_idx: usize) -> usize {
let curr_node = &mut self.nodes[curr_node_idx];
*curr_node.suffix_link.get_or_insert_with(|| {
if curr_node_idx == 0 || curr_node.parent == 0 {
0
} else {
let next_link = self.get_link(curr_node.parent);
let next_node = self.go(next_link, curr_node.ch.expect("cannot be empty, only root node.ch is empty"));
next_node
}
})
Как нетрудно заметить, я сразу пытался сделать максимально быструю реализацию. Entry API для доступа к элементам словаря, чтобы не перехеэшировать и не искать место для вставки лишний раз! Вынести доступ к массиву отдельно, чтобы избежать повторных проверок выхода за границы массива! get_or_insert_with чтобы красиво поменять Option
на вычисленное значение и сразу вернуть его! Все замечательно, только не работает, зараза такая.
И самое то страшное в том, что компилятор-то прав. Если all_links
из Entry::Vacant(all_links)
используется после рекурсивного вызова self.go
, то во время этого вызова в curr_node.all_links
может быть добавлен новый ключ, в словаре может закончиться место, он может быть переалоцирован, и теперь all_links
указывает непонятно куда.
И мне вспомнилась статья Fast Development In Rust, Part One. И я переписал все максимально прямолинейным и дуболомным способом. Никакого entry
, .get().is_some()
. Копипаста self.nodes[curr_node_idx]
везде. .expect
для получения значения из Option
. И код скомпилировался.
Компилятор Rust достаточно умный. У него в заначке есть, например, Non-lexical lifetimes. И он понял, что некоторые ссылки на самом деле живут недостаточно долго для того, чтобы на самом деле вызвать проблемы. И вот теперь с компилирующимся кодом я решил попробовать вернуть что-то назад. И уже минут через 5-10 я вернул примерно половину оптимизаций назад!
Заголовок спойлера
fn go(&mut self, curr_node_idx: usize, ch: char) -> usize {
assert!(curr_node_idx < self.nodes.len());
let curr_node = &mut self.nodes[curr_node_idx];
if let Entry::Vacant(all_links) = curr_node.all_links.entry(ch) {
if let Entry::Occupied(next) = curr_node.next.entry(ch) {
all_links.insert(*next.get());
} else {
let new_link = if curr_node_idx == 0 {
0
} else {
let next_link = self.get_link(self.nodes[curr_node_idx].parent);
self.go(next_link, ch)
};
self.nodes[curr_node_idx].all_links.insert(ch, new_link);
}
}
self.nodes[curr_node_idx].all_links[&ch]
}
fn get_link(&mut self, curr_node_idx: usize) -> usize {
assert!(curr_node_idx < self.nodes.len());
if let Some(link) = self.nodes[curr_node_idx].suffix_link {
link
} else {
let new_link = if curr_node_idx == 0 || self.nodes[curr_node_idx].parent == 0 {
0
} else {
let next_link = self.get_link(self.nodes[curr_node_idx].parent);
self.go(next_link, self.nodes[curr_node_idx].ch.expect("cannot be empty, only root node.ch is empty"))
};
self.nodes[curr_node_idx].suffix_link = Some(new_link);
new_link
}
}
Для меня не была важна скорость работы, я просто изучал, как алгоритм работает. Вряд ли бы я стал его реализовывать самостоятельно для какой-нибудь реальной задачи. Он уже реализован и оптимизирован так, что мне с вот этим смехом и не снилось. Но вот хотелось. Сэкономить аллокацию, лишнее хэширование, проверку выхода за границу массива. Но это все были преждевременные оптимизации, которые при этом помешали мне написать рабочий код.
После этого я решил еще попробовать реализовать построение суффиксных ссылок с помощью BFS т.к. так и не смог понять, что же в этой горе рекурсивных вызовов go
и get_link
происходит и, вооружившись новыми знаниями, написал самую простую и прямолинейную реализацию BFS, которая пришла мне в голову.
Заголовок спойлера
fn bfs_suffix_links(&mut self) {
let mut queue = VecDeque::new();
queue.push_back(0);
while let Some(curr_node_idx) = queue.pop_front() {
queue.extend(self.nodes[curr_node_idx].next.values());
if self.nodes[curr_node_idx].ch.is_none() || self.nodes[curr_node_idx].parent == 0 {
continue;
}
let current_node_char = self.nodes[curr_node_idx].ch.expect("only root node has empty .ch");
let parent_node = self.nodes[curr_node_idx].parent;
let parent_suffix_link = *self.nodes[parent_node].suffix_link.get_or_insert(0);
if self.nodes[parent_suffix_link].next.get(¤t_node_char).is_some() {
let new_suffix_link = *self.nodes[parent_suffix_link].next.get(¤t_node_char).expect("already checked");
self.nodes[curr_node_idx]
.all_links
.insert(current_node_char, new_suffix_link);
}
}
}
И она скомпилировалась. Правильно с первого раза не заработала, пришлось поискать и исправить логические ошибки. Но потом код стал и компилироваться, и работать, как надо.
Если бы это был код на Python, который мне надо проверить перед мерджем, я бы сказал переделать многое из этого. В Rust же, скорее всего, нет. Если этот код будет бутылочным горлышком и потребует оптимизации — это будет несложно. Что я и сделал:
Заголовок спойлера
fn bfs_suffix_links(&mut self) {
let mut queue = VecDeque::new();
queue.push_back(0);
while let Some(curr_node_idx) = queue.pop_front() {
assert!(curr_node_idx < self.nodes.len());
let curr_node = &mut self.nodes[curr_node_idx];
queue.extend(curr_node.next.values());
// Root node
let Some(current_node_char) = curr_node.ch else {
curr_node.suffix_link = Some(0);
continue;
};
// Root children
if curr_node.parent == 0 {
curr_node.suffix_link = Some(0);
continue;
}
let parent_node = curr_node.parent;
let parent_suffix_link = *self.nodes[parent_node].suffix_link.get_or_insert(0);
if let Some(&new_suffix_link) =
self.nodes[parent_suffix_link].next.get(¤t_node_char)
{
self.nodes[curr_node_idx]
.all_links
.insert(current_node_char, new_suffix_link);
}
}
}
Как я и говорил выше, после того, как код скомпилировался и начал правильно работать, его рефакторинг становится невероятно простым. Просто меняешь строчку за строчкой и смотришь, как на это реагирует компилятор. Если ошибок нет, то все отлично практически на 100%. Если ошибки есть, то, может быть, это небезопасно, а может, надо просто использовать что-то другое. В этот момент я понял, что мне это напоминает. Это же TDD!
По сути, в Rust по умолчанию TDD (наверно, в данном случае это должно быть CDD — compiler-driven development, разработка через компилирование), просто вместо тестов — компилятор. Сначала пишешь достойную фильма ужасов кучу как-то работающего кода, а потом приводишь её в нормальный вид. Такой подход позволяет писать и быстро, и качественно одновременно. Именно в этот момент в моей голове возникла структура этой статьи, и мне захотелось её написать. Если это будет единственное, что из этой статьи останется у вас в голове, то, прошу, запомните эту часть, все остальное было просто очень длинной подводкой к этой мысли.
Как оказалось, не только у меня такие мысли по поводу Rust. Во время дописывания этой статьи я нашел выступление Beyond Safety and Speed: How Rust Fuels Team Productivity от Lars Bergstrom, Google Android Director of Engineering на конференции Rust Nation UK 2024. Рекомендую его посмотреть полностью, это всего 30 минут. Вот несколько фактов:
— Rust-команды настолько же продуктивны (как в разработке кода, так и в поддержке), как и Golang команды и более чем в 2 раза более продуктивны, чем C++ команды;
— 2/3 опрошенных разработчиков сообщили, что спустя 2 месяца (или меньше) были достаточно уверены в знании Rust для участия в проектах;
— 85% опрошенных разработчиков сообщили, что у них выросла уверенность в корректности кода по сравнению с другими языками программирования.
Я даже не уверен, что тут можно еще добавить, выступление говорит само за себя.
С чем действительно сложно спорить, так это с тем, что большие Rust-проекты компилируются долго. Иногда очень долго.
Примеры инструментов для ускорения компиляции
— cargo-wizard, который позволяет удобно настраивать профили для компиляции;
— Cranelift — альтернативный компилятор, который в некоторых случаях ускоряет дебаг компиляцию.
Но даже со всем этим (и тем, что появится в будущем) я не думаю, что Rust будет компилироваться так же быстро, как Golang. И если вы Google (или компания схожего размера) то суммарно на все десятки тысяч программистов вы платите за время компиляции очень много. Для простоты даже давайте проигнорируем тот факт, что если программист ждет компиляции, то это не значит, что он не работает над чем-то другим. Задайте себе вопрос: «А влияет ли на вашу компанию скорость компиляции?» Или еще лучше: «А что больше: время, потраченное на компиляцию корректного кода, или время, потраченное на дебаг быстроскомпилированного кода?» У меня нет однозначного ответа на эти вопросы, у всех свои приоритеты.
Часть 6. Сложность
Основная часть статьи закончена, поздравляю, вы смогли дочитать до «сцены после титров». И раз уж мы все тут собрались, то давайте я заодно расскажу, почему я не считаю, что «Rust сложный». И я даже не имею в виду «Rust простой, это окружающий мир сложный», хотя в этом есть своя доля правды (пример с обработкой возможных ошибок IO выше наглядно это показывает). Я считаю, что Rust — простой язык. Я даже больше скажу: я считаю, что Rust скучный (надеюсь далее переход простой -> скучный станет понятнее).
В коде на Rust практически нет неожиданностей. А если есть, то обычно это что-то вроде «О, оказывается и для этого есть специальный метод» или «линтеры даже такое теперь подсказывают». А вот когда я пытаюсь разобраться в коде, то редко удивляюсь (хотя исключения бывают). Практически с одного взгляда понятно, что где в памяти находится, что с этим можно сделать, почему вот это возможно, почему вот этот код не работает. Даже самый ужас для всех новичков в Rust — аннотации лайфтаймов — на самом деле несложные, да и нужны только тогда, когда надо связать лайфтаймы друг с другом.
Писать корректный и безопасный код на Rust несложно, особенно с учетом помощи компилятора. Я уже рассказал про CDD, но это можно сравнить еще с одним подходом — парным программированием. Часто для ошибок, которые типичны для новичков, компилятор пишет, как исправить код прямо в тексте ошибки (это не на 100% правильно, иногда бывает так: в этом конкретном случае надо сделать что-то другое, а компилятор предлагает решение не той проблемы).
В доказательство того, что Rust простой, я предлагаю вам статью Grading on a Curve: How Rust can Facilitate New Contributors while Decreasing Vulnerabilities.
A first-time contributor to a C++ project was approximately 70 times as likely to introduce a vulnerability as a first-time contributor to an equivalent Rust project. This provides strong evidence that even if one were to accept that Rust is a more difficult language to learn than C++, it can still provide a sizable net benefit to new contributors to such projects
Заголовок спойлера
Для протокола: в статье есть график зависимости вероятности добавления уязвимости к «опыту», опыт там — это количество коммитов в проект, а не общий опыт использования конкретного языка.
Мне кажется, что это невероятное достижение! Может быть, кривая обучения в Rust — это такая ступенька, высота которой равна сложности скомпилировать код. Это позволяет новичкам в проекте (или новичкам в Rust) быть более уверенными в качестве своего кода и не бояться его писать. В Rust, если код скомпилировался, (а еще лучше и тесты прошли и новые написаны) то это достаточно хороший индикатор того, что код можно смотреть ревьюеру, и совсем неработающим он не будет.
Но тогда почему многие люди (в том числе из тех, кто пишет на Rust профессионально) говорят, что он сложный? Мне кажется, тут 2 основных момента:
— если изучать Rust как «быстрый и низкоуровневый» (т.е. как безопасный C), то появляется подсознательное желание писать производительно. Экономить аллокации, избегать .clone()
, Rc<RefCell>
и Arc<Mutex>
и т.д. Выше я показал, что если начинать с простого кода, то можно быстро писать быстрый код.
— если все же надо выжимать каждый такт процессора, то тогда все становится сложно очень быстро.
Как и в классической схеме «быстро, качественно, дешево» в Rust есть «просто, корректно, производительно». Можно еще четвертым пунктом добавить «удобно для использования», но как в анекдоте «добавить-то можно, но выбрать все равно можно только 2».
Про «просто, корректно и непроизводительно» (скорее не максимально производительно, Rust все равно будет быстрее большинства других языков) написано выше, про «просто, производительно и некорректно» все понятно. Давайте еще рассмотрим «корректно, удобно и сложно» и «корректно, производительно и сложно» с unsafe
.
Корректно, удобно и сложно
Давайте разберем аргументы функции read_dir (функция открытия папки). Единственный аргумент у этой функции — это путь, так какой же нам выбрать для этого тип?
Начнем с простого: в Rust есть строки, т.е. String. Но это плохой вариант. В Rust строки гарантированно UTF-8, а путь — совсем необязательно (эта часть вдохновлена статьей I want off Mr. Golang's Wild Ride и почему просто UTF-8 строки — это плохой выбор для пути, можете почитать там, а мы идем дальше).
Хорошо, в Rust есть OsString, он как строка, только никаких гарантий кодировки там нет, просто вектор байт. Подождите, но вектор же должен аллоцироваться в куче! Это что, нам, чтобы папку открыть, надо еще и в куче что-то аллоцировать? Звучит как-то не очень. Идем дальше.
Дальше мы находим &OsStr, который относится к OsString
, как &str
относится к String
, т.е. это просто толстый указатель (указатель + длина в данном случае) на последовательный набор байт. А эта самая последовательность может быть где угодно: в куче, на стеке, зашита в нашем исполняемом файле и т.д. Но это все равно не идеальный вариант, это все еще строка, т.е. для &OsStr
определены методы +- как у строки, а мы же не первобытные люди, мы живем в 21 веке, кто в 2024 году конструирует пути форматированием строк?
Точно не мы; нам нужно что-то еще, а именно тип &Path. Это уже совсем путь, с методами для путей, внутри у него знакомый нам OsStr
, так что тут все тоже хорошо. Но и это еще не конец, мы можем сделать еще лучше!
Представьте, что нам надо открыть папку с константным путем, который будет представлен как &str
. И что, нам теперь надо как минимум импортировать Path
, а может, еще и OsStr
, чтобы этот Path
собрать? Это же ужас как неудобно, давайте в качестве пути передавать не просто &Path
, а AsRef<Path>
, т.е. дженерик. Сам по себе AsRef
дает метод as_ref()
, который нужен для того, чтобы делать дешевые конвертации указателей.
Давайте посмотрим еще раз на всю нашу цепочку:
— String
-> OsString
всегда валидно, т.к. данные у них одинаковые, и у String
строго больше гарантий;
— OsString
-> &OsStr
тоже всегда валидно, т.к., по сути, если у OsString
выбросить поле capacity
, то у нас и получится &OsStr
;
— &OsStr
-> &Path
очевидно валидно, т.к. Path
просто содержит OsStr
как единственное поле, разница у них только в методах.
Тогда получается, что мы можем все эти типы (а еще и &str
) дешево конвертировать в &Path
. А значит, мы можем принять в нашу функцию все вышеперечисленное и внутри это привести к нужному нам виду.
Фух, это было долго, но это показывает, насколько авторы стандартной библиотеки задумывались над тем, как же ей будут пользоваться. Во многих языках нет даже намека на подобное внимание к деталям, причем даже в гораздо более фундаментальных аспектах (Golang и zero values). Во многих языках путь — это просто строка. Ведь это так удобно — собирать пути по кускам с помощью форматирования строк.
Весь этот пример тут для того, чтобы показать: если хочется писать лучший код, то он действительно становится сложным и над ним приходится думать. И мне кажется, что это правильно, когда создатели стандартной библиотеки (или просто библиотеки) думают над тем, как их библиотекой будут пользоваться.
Unsafe
Писать корректный unsafe
код действительно трудно. Труднее, чем писать на C, т.к. в Rust необходимо поддерживать больше инвариантов. Почему наличие unsafe
не проблема, а достоинство Rust, почему писать unsafe
код сложно, почему даже простое изменение поля структуры может быть unsafe и многое другое гораздо лучше расскажет уже упомянутый Алексей Кладов. Я только скажу: если считаете, что Rust сложный из-за того, что в нем надо писать unsafe
код, то его можно не писать, нужных применений у него очень ограниченное количество. Если же считаете, что сам факт наличия unsafe
делает Rust сложным (или небезопасным), то используйте либо 100% safe
или проверенные временем популярные библиотеки в своем коде.
Еще возможно, что когда говорят о сложности имеют в виду "размер" языка. Обычно про что-то подобное говорят в контексте C или Golang, мол, вон какое все маленькое, все можно быстро выучить и ты уже весь язык знаешь! Этот аргумент я не понимаю совсем. Что под размером подразумевается тоже от меня ускользает. Количество ключевых слов? Размер стандартной библиотеки? Что бы это ни значило, подобная метрика не выглядит полезной. Ни C, ни Golang не являются простыми, размер им тут не помогает. Ядро атома тоже маленькое и состоит из небольшого (относительно) количества элементов, но простым это его не делает.
По такой логике Brainfuck является одним из самых простых языков. На весь язык всего 8 команд; если захотеть, все «обучение» можно скомпоновать в одно предложение, проще некуда. Проверку того, насколько легко Brainfuck писать, дебажить и рефакторить, я оставляю заинтересовавшимся читателям в качестве домашней работы, моей нервной системы на это не хватит.
Rust же не пытается быть маленьким. У одного Result
я сейчас насчитал 39 методов. Мне не кажется, что это делает его сложным для понимания. Посмотреть описание метода — дело от силы 30 секунд, зато дальше знаешь, что это и зачем (про IDE я просто молчу, максимум надо курсор навести).
Кстати о Result
. Монады — это не сложно. Сколько раз я читал статьи про монады, смотрел видео про них, но я до сих пор не помню, кто такой функтор и куда он морфирует. Что абсолютно не мешает использовать их в коде. Это же просто контейнер с методами! Или это интерфейс взаимодействия с данными. Никто, вроде, еще от шока при виде структуры или класса еще не умер, а чем монады-то сложнее? Слово разве что сложное, на этом сложность и заканчивается.
Часть 7. Заключительная
Если кто-то дожил до этих строк — большое вам спасибо за ваше время. Надеюсь, вы не посчитаете, что потратили его зря. Я не ожидал, что у меня получится написать 40к символов текста, но как-то это получилось.
Если у вас есть какие-то вопросы, пишите их в комментариях, постараюсь ответить.
И по традиции для постов на хабре: подписывайтесь на мой телеграм... а, у меня же нет никакого канала, простите, слишком много я читаю хабр вместо полезной работы.
Комментарии (499)
Tuvok
02.04.2024 18:49+1Таки Rust лучше C/C++ ? Или для каких-то конкретных применений? UB это на самом деле так страшно или, к примеру, в Arduino с ним никогда и не столкнёшься?
UranusExplorer
02.04.2024 18:49+21UB это на самом деле так страшно или, к примеру, в Arduino с ним никогда и не столкнёшься?
UB это страшно. В том числе и потому, что оно может быть в коде, но вы с ним не "столкнетесь" до тех пор пока не обновите компилятор или его опции, не поменяете что-то в другой части кода или просто не сложатся звёзды на небе случайным образом. От платформы это слабо зависит.
MiyuHogosha
02.04.2024 18:49и оно может быть в коде компилятора, поэтому два откомпилированных бинарника с одного и того же кода могут быть разными. Но и в расте до... UB. Не говоря уже о уязвимостостях заложенных в генерацию кода, особенно с borrow. Забавно что на расте в отличии от плюсов гораздо легче сымитировать ошибки с обновлением кэша.
Lex98 Автор
02.04.2024 18:49+7Можете привести какой-нибудь пример? Потому, что пока не очень понятно о чем вы.
Tuvok
02.04.2024 18:49+1Ну хотелось бы примеров для того же условного ардуино, где можно случайным образом задействовать UB, особенно учитывая что это по сути embedded разработка, где каждую переменную выверяешь и действительно вручную проверяешь память чтобы не забилась, потому как там её кот наплакал. И взаимодействие там real-time ибо исполнительные механимзы, так что в проде это чревато не просто каким-то неработающим сервисом, а поломкой всей системы, причем в механическом смысле тоже.
UranusExplorer
02.04.2024 18:49+4UB - это не обязательно только про обращение к неинициализированному или некорректному участку памяти. UB - это, например, переполнение знаковой интовой переменной. Да, вы можете предположить, что есть у вас signed integer и вы прибавляете к нему +1, то компилятор просто сгенерирует процессорную инструкцию инкремента или сложения, и зная как представлены числа на архитектуре предположить, что если у вас есть значение в переменной INT_MAX, вы к ней прибавите +1, то получите INT_MIN. А потом вы где-нибудь напишете if (X+n)>X, чтобы проверять не вызовет ли операция переполнение. И оно даже скорее всего так будет работать на протяжении многих лет. Но проблема в том, что стандарт языка не даёт таких гарантий! Вообще не даёт. Более того, согласно стандарту языка, такой код вообще не является корректным кодом. И если компилятор обнаружит а коде что-то подобное, то он имеет полное право в таком случае сгенерировать вообще все что угодно - вплоть до того, что этот if будет выдавать и true и false в разных частях кода для одинаковых значений X и N (пример там есть по ссылке), компилятор также что выкинуть какой-то находящийся рядом код или сгенерировать инструкции которые делают вообще не то что вы подразумевали. И то, что он не делает это сейчас, совершенно не гарантирует, что это не произойдет когда-нибудь в будущем (стоит вам обновить компилятор, изменить опции компиляции или даже поменять что-то в коде по соседству). А если у вас будет что-то типа такого в условии цикла, то бесконечный цикл может перестать быть бесконечным, или наоборот (см. пример).
И это только один из сотен примеров. UB - это не только про память и многопоточность, увы.
Довольно подробные объяснения с примерами можно найти тут:
Неопределенное поведение и правда не определено
Неопределенное поведение может привести к путешествиям во времени
Tuvok
02.04.2024 18:49Простите, видимо неверно сформулировал вопрос, опять же касаемо конкретных применений. То что вы пишете, скорее всего имеет место быть в языке вообще, но в конкретном применении конкретной платформе это может быть не так. Вопрос был про Wiring и avr-gcc, где даже многозадачность делается костылями вроде программного таймера, основанного на аппаратном с типом unsigned long (uint32_t) и такие костыли даже переживают переполнение переменной в исходной функции. Но эти фишки известны до компиляции и как раз эксплуатируют такое поведение. Это я возвращаясь к вопросу о том как же можно "случайным образом задействовать UB" в конкретной области применения. А конкретная область применения это статическая линковка и известность того что может прилететь с датчиков на входы и нет никаких внешних сервисов с которых может прилететь что-то недопустимое. Вот и интересно, в таких жёстко заданных платформой рамках, возможно ли столкнуться со случайным UB или это настолько маловероятно, что бояться этого не стоит.
UranusExplorer
02.04.2024 18:49+9То что вы пишете, скорее всего имеет место быть в языке вообще, но в конкретном применении конкретной платформе это может быть не так.
Нет, нет и ещё раз нет. UB не зависит от платформы или применения. Вообще не зависит - UB в коде либо есть, либо его нет, вне зависимости от платформы.
Случаи undefined behavior описаны в стандарте языка, их там несколько сотен. Код, в котором есть UB по определению не является корректным кодом на языке C или C++. Более того, в случае наличия UB в коде компилятор имеет полное право сгенерировать любую хрень из вашего кода, и это вообще не зависит от аппаратной платформы и ее ограничений.
Вы, кажется, не совсем верно понимаете, что такое UB. UB это не "я обращаюсь к неинициализированной переменной в куче, но на моей платформе память изначально заполнена нулями, значит в ней будет ноль". UB - это то, про что в стандарте языка явно сказано "так делать нельзя", и если компилятор увидит такое в коде, он, например, может вообще выкинуть условие if (a == 0) и все что внутри него. Имеет полное право. Если на вашей платформе инкремент знакового двухбайтового инта 32767 сделает его равным -32768 (на большинстве платформ так), то увидев if ((x+1)>x) в коде компилятор может выкинуть эту проверку и заменить везде на true, потому что согласно стандарту языка переполнение знаковых недопустимо. И так далее. Наличие или отсутствие UB в исходном коде не зависит от платформы, под которую вы компилируетесь.
Ну и полагаться на "у меня в коде UB, но для моей платформы компилятор генерит правильный машинный код" очень опасно. Сегодня генерит, а завтра что-то изменится, и уже не будет. Потому что имеет полное право.
Tuvok
02.04.2024 18:49Как я понимаю, UB делалось ради переносимости кода между платформами, оставляя конкретное поведение на совести компилятора. Соответственно если конкретная платформа убирает UB из своего компилятора, то это делает программу непереносимой, а раз так то программа уже не будет называться написанной на языке "Х", потому возникает диалект языка, привязанный к платформе, и назовут этот диалект языком "Y". Тогда и получим отсутствие исходного UB языка "X" в языке "Y", несмотря на то что по сути это один и тот же язык, просто этот исходник не скомпилируется другим компилятором под другую платформу (или скомпилируется, но выдаст то самое UB про которое вы говорите, но делающий такое уже ССЗБ). Если же, как вы говорите, поменяется что-то в компиляторе, то скорее всего либо разработчики компилятора будут опираться на предшествующее отсутствие UB либо уже железка будет переделана под новое поведение.
Конечно же это всё может быть и совсем не так (всякое случается), поэтому будет интересно, стоит проверить приведённую вами конструкцию UB в конкретной среде.
mayorovp
02.04.2024 18:49+4Да, но нет. Делалось-то для переносимости, но это совершенно не означает, что на конкретной платформе все UB оказались до-определены.
Компиляторы с удовольствием пользуются некоторыми видами UB для оптимизации кода; в таком случае поведение остаётся неопределённым даже если зафиксировать платформу и версию компилятора.
creker
02.04.2024 18:49+2По-моему в C изначально UB именно для этого и было и означало скорее implementation defined поведение. Т.е. язык как бы не при делах, но в конкретных условиях все ок. В те времена C еще можно было считать человекочитаемым ассемблером.
Сейчас же UB предназначено целиком и полностью для оптимизации, чтобы у компилятора была свобода ломать код так, как ему вздумается. Платформа здесь не имеет значения, потому что компилятор не смотрит на то, что в какой-то системе two's complement представление чисел. Для него UB это UB на любой платформе. Собственно, для него вообще платформы конечной не существует - он работает с абстрактной машиной. Поэтому как выше писали, проверка на переполнение будет просто удалена компилятором. Поэтому и версия компилятора может запросто сломать то, что работало раньше, если оно опиралось на UB.
UranusExplorer
02.04.2024 18:49+2оставляя конкретное поведение на совести компилятора.
Неа. То, что вы сказали - это не undefined behavior, а unspecified behavior, в стандарте оно тоже есть.
если конкретная платформа убирает UB из своего компилятора
Почти, обычно UB "убирает" не конкретная платформа, а конкретный компилятор. Например GCC и Clang делают исключение (точнее, расширение языка) для упомянутых выше union, разрешая в них puning простых типов. Но пользоваться таким можно только если разработчиками компилятора явно явно сказано "в стандарте C++ это UB, но мы реализуем нестандартное расширение языка, в котором это не UB". Если такого не сказано, то писать код таким образом под этот компилятор нельзя.
просто этот исходник не скомпилируется другим компилятором под другую платформу
Скорее всего скомпилируется, нередко даже без варнингов. Но никаких гарантий, что он всегда будет работать правильно уже не будет.
В этом и опасность UB - вы можете даже не подозревать, что оно есть у вас в коде до определенного момента.
Если же, как вы говорите, поменяется что-то в компиляторе, то скорее всего либо разработчики компилятора будут опираться на предшествующее отсутствие UB
Это только в том случае, если разработчики специально "определили" это конкретное UB и больше не считают его UB в своей реализации. А если в прошлой версии это UB было именно UB, то в новой версии тоже возможно все что угодно. То, что работало, может перестать работать. А может наоборот начать работать то, что раньше не работало :) на то оно и undefined
стоит проверить приведённую вами конструкцию UB в конкретной среде
Это ни о чем не скажет. Оно может отлично работать в вашей конкретной среде. До тех пор, пока вы не поменяете что-то в опциях компилятора, не обновите компилятор до более новой версии, или даже не поменяете что-то в какой-то другой части исходника.
ahabreader
02.04.2024 18:49Но пользоваться таким можно только если разработчиками компилятора явно явно сказано "в стандарте C++ это UB, но мы реализуем нестандартное расширение языка, в котором это не UB".
Это плохой пример из-за нелепости ситуации вокруг type punning'а через union. Комитет решил сломать совместимость* с C в этом месте, а разработчики (возможно, всех существующих) компиляторов - нет.
Если стандарт игнорирует реальность, тем хуже для стандарта, а не для реальности. Тут нужен ещё один пример.
Он также игнорирует реальность, когда требует поддержку исключений и RTTI во freestanding-реализациях (так на языке стандарта называется bare metal). Комитет почему-то изначально (в C++98) послал таким образом много платформ и там до сих есть люди, голосующие за сохранение ситуации**. @Tuvok спросил про Arduino, а самое первое, что можно сказать об avr-g++ и стандартности: стандарт игнорирует существование этой платформы (потому что не хочет учитывать существование -fno-exceptions).
Так проявляются некие трения насчёт совместимости с C и насчёт поддержки bare metal в комитете и в представлениях Страуструпа***. Можно с ними не согласиться, как и с мнением Страуструпа о C или C++ на 8-битных микроконтроллерах: "Probably best stick to assembler".
В общем, на это могут смотреть не как на явное расширение компилятора, а как на "вы не рискнёте по дурости комитета сломать весь софт, пользующийся этим приёмом: x265, Firefox, Qt, OpenCV, ClickHouse, Boost... (точно так же, как не добавите в компилятор для bare metal неотключаемые исключения ради соблюдения стандарта - это же глупо)".
* в стандарте C поведение описано на этой странице (ctrl+F: type punning), в стандарте C++ это уже UB.
** голоса 1 Against, 2 Strongly Against в голосовании "We support proposed removal..."
*** Страуструп 2002: "Remove all incompatibilities: This is my ideal", Страуструп 2024: "Unfortunately, unions are commonly used for type punning".
Aldrog
02.04.2024 18:49+1Для того, что вы описываете, в стандарте есть отдельное понятие - unspecified behaviour (да, тоже UB, но обычно когда пишут UB, подразумевают undefined behaviour). Наличие в коде unspecified behaviour означает потенциальные проблемы с переносимостью, но просто от обновления компилятора или изменения никак не связанного участка кода ничего не сломается.
А undefined behaviour это именно "программа, допускающая такое поведение, некорректна". Я слышал, что изначально при стандартизации комитет просто не захотел принимать никаких решений о том, какое поведение должно быть в этих ситуациях, а потом компиляторы научились делать крутые агрессивные оптимизации, исходя из того, что UB не может происходить, и в результате наличие в стандарте UB стало принципиальной позицией.
Rezzet
02.04.2024 18:49+11Почитал статью и так и не понял чем раст лучше, наверно это неплохой язык, но что мешает использовать пул потоков в с++, почему автор статьи считает что многопоточное программирование на с++ это потоки и мьютексы?
Мое понимание разработки на данный момент сводится к тому что можно очень долго выяснять какой язык лучше и чем на, скажем так, академических примерах, беда в том что это сильно далеко от реальной жизни. Мне не нужен язык, мне нужно программу делать, а для этого нужны библиотеки, очень много библиотек, на данный момент в составе vcpkg около 2,5 тысяч библиотек. Это код который у вас сразу и скорее всего без проблем соберется под все распространённые настольные и мобильные платформы. Не представляю что должно случиться что бы библиотека CGAL была переписана на другой язык. В нее закопаны наверно сотни человеколет работы. И это только одна библиотека из множества других. И такие библиотеки можно перечислять очень долго: SQLite, Qt, curl, boost graph, openimageio, eigen3, lua, imgui... У меня в глазах ужас при мысли как каждую из них можно было бы портировать на другой язык, не говоря уже о всех вместе.
gmtd
02.04.2024 18:49+1В последнее время пишу в основном фронтенд на js. В реестре npmjs.org более 2 миллионов пакетов. Тем не менее пишу сложные веб приложения используя только 2-3 из них (кроме фреймворка), да и от тех при желании можно избавиться. Те, кто использует много библиотек через пару лет, имеет огромные проблемы с апгрейдом и поддержкой приложения
В С++ не так?
SparkyJoyteon
02.04.2024 18:49+3Плюс минус так же, много библиотек в проект не затянуть. Автор комментария скорее ведёт к тому что Раст просто беден на качественные библиотеки что смогли бы закрыть потребности программистов, как с этим сейчас успешно справляются плюсы
Lex98 Автор
02.04.2024 18:49+6Кстати, раз уж про поддержку зависимостей заговорили. В Rust есть потрясающая возможность иметь одну библиотеку нескольких разных версий без конфликтов (если не надо между разными версиями взаимодействовать). Так что если ваша зависимость A имеет подзависимость C версии 1.0.0, а в зависимости B подзависимость C версии 2.0.0, то они спокойно продолжат работать. Можно даже импортировать свою собственную библиотеку другой версии (это используется, например, чтобы пофиксить баг только в самой новой версии, а в старых просто импортировать из новой и не поддерживать несколько версий фиксов).
Buzzzzer
02.04.2024 18:49+3Звучит, так, как будто это отличная фича, чтоб выстрелить себе в ногу.
blind_oracle
02.04.2024 18:49+6Нет, на самом деле это офигенно и позволяет продолжать писать код, даже если прямая зависимость А уже обновилась чтобы использовать косвенную зависимость С версии 2.0, а зависимость B ещё на 1.0.
В плюсах это, я думаю, сразу ставит крест на такой комбинации - форкайте B и переписывайте на 2.0 или откатывайте А на старую версию.
Lex98 Автор
02.04.2024 18:49+2Насколько я знаю, есть одну неожиданную проблему, которую это может вызвать - если все же библиотеки A и B обмениваются типами из C, то можно получить ошибку
expected struct C::InnerType, found a different struct C::InnerType; note: perhaps two different versions of crate
Care being used?
В остальном, насколько я знаю, проблем это не вызывает.Gorthauer87
02.04.2024 18:49+1Есть и вторая уже более интересная проблема. Все это ломается, когда надо цеплять библиотеку, зависимую от Си. Там нельзя иметь мультиверсию смешной зависимости.
Shatun
02.04.2024 18:49+1Много раз сталкивался с такой проблемой в других языках.
Например библиотека А имеет зависиость на B 1.0, а C ссылается на B 2.0. Всегда было больно, из всех моих случаев поддержка разных версий зависимости решило бы проблему(в часте случаев так и фиксилось - пинилась старая версия какими-то костылями)Buzzzzer
02.04.2024 18:49Проблема есть, я ж не отрицаю.
Но не будет ли больно искать багу, когда окажется что в каких-нибудь тестах и в разных местах программы, с виду, одинаковый код, который, совсем неочевидно, будет работать по разному ?
Можно конечно будет сказать — это %somecoder% во всё виноват, а не инструмент, но время будет потеряно.Lex98 Автор
02.04.2024 18:49Можете привести пример, по вашему описанию не очень понятно, какая возможна бага.
Rezzet
02.04.2024 18:49+2Нет, не так, с++ очень стабилен в поддержке и всегда имеет обратную совместимость(был один или два случая когда это правило нарушилось). Наверно есть библиотеки которые могут перестать работать, но таких случаев не помню. Многие с++ библиотеки слишком большие что бы быть заброшенными. Например curl это часть операционной системы линукс и с некоторых пор виндовс. Qt развивается с 93 года или что-то около того. Многие библиотеки с++ это матрешки, которые зависят от более мелких библиотек, а те еще от более мелких. К примеру openimageio зависит от 98 других библиотек. Так же в с++ очень мало зоопарка библиотек, никто не будет писать аналог zlib(библиотека сжатия данных).
Бегло посмотрел репозиторий что вы дали, нашел интересную штуку JoltPhysics.js( A WebAssembly port of JoltPhysics) это то о чем говорил, с++ проникает условно в мир других языков через порты и обертки под эти языки, уверен есть порт для питона и других языков, но внутри там будет с++ библиотека.
Еще почему-то в подобных разговорах принято считать что программисты с++ думаю только о нем и ничего вокруг не видят, в "мире с++" вполне нормальная практика брать и вкручивать в приложение другие языки, почти все игровые движки созданные на с++ имеют внутри себя интерпретаторы(или jit компиляторы) других языков для упрощения написания логики, как правило это lua или python, Unreal Engine внутри содержит графический язык BluePrint. Qt сейчас переходит на QML это использование с++ для базовых функции и склеивание их через js.
Так же нет проблемы двигаться вниз по языковым абстракциям и спускаться до ассемблера, если есть желание использовать какие-то низкоуровневые процессорные расширения типа AVX, почти все видео кодеки туда спускаются, но это порождает определенные трудности в переносимости под другие типы архитектур.
gmtd
02.04.2024 18:49Мой пойнт был в том, что важных библиотек (типа графических или curl), которые используются в проекте - их очень немного. И они довольно оперативно и качественно портятся на другие языки/фреймворки когда надо. А "мелочь" можно реализовать самому.
Опять же - так в мире js и npmjs
То есть утверждение, что уже написано куча либ под что-то и что нам делать и будет куча проблем при переходе на Rust - необосновано
Lex98 Автор
02.04.2024 18:49+12Почитал статью и так и не понял чем раст лучше, ..., но что мешает использовать пул потоков в с++
Видимо у меня не получилось расставить акценты в нужных местах. Преимущество Rust не в том, что есть библиотека, в которой пул потоков есть (и даже не в идиоматичном варианте, когда для того, чтобы сделать код многопоточным надо добавить
.par_bridge().into_par_iter()
и все), а в том, что компилятор гарантирует отсутствие гонок данных. Этот пример важен не самим кодом, а текстом после, т.е. пока я писал этот пример я сделал много разных ошибок, которые бы не помешали скомпилироваться C и/или C++ коду, но помешали скомпилироваться коду на Rust. Сила Rust в гарантиях компилятора, который позволяет экспериментировать. В safe невозможно скомпилировать программу там, чтобы передать одну задачу в 2 разных потока и потом дебажить гонки данных (можно послать копию задания, но это будет просто бесполезной работой, а не UB). Я могу позволить себе попробовать написать код без мьютексов, т.к.если я сделаю в нем ошибку, код просто не скомпилируется. В большинстве других языков конкурентность связана с огромным количеством граблей, которые приходится ловко обходить.curl
Curl не то, что бы прям переписывают на Rust, но как минимум некоторые новые части пишутся на Rust. Если интересно, можете почитать про недавнюю уязвимость в curl, там в том числе и про переписывание на Rust.
SQLite
Не уверен, насколько они production ready, но на Rust пишут и базы данных, и key-value хранилища и прочие вещи, но сам язык в 3 раза моложе SQLite, так что трудно винить в этом язык.
Все же, "переписать все на Rust" это не то, что бы реальная цель или задача. Код, который полировали десятилетия никто просто так выкидывать не будет (уж точно не в первую очередь). Я поэтому и написал, что для меня у C и/или C++ ниша одна - легаси код, который слишком долго или дорого переписать.
в составе vcpkg около 2,5 тысяч библиотек
В Cargo.io на момент записи 141,981 пакетов. Это не честное сравнение, Rust поощряет разбиение пакетов на много мелких для переиспользования и ускорения компиляции, так что это не 141к обособленных пакетов, но все же.
SabMakc
02.04.2024 18:49Rust не гарантирует отсутствие гонок.
Rust следит за некоторыми популярными проблемами с многопоточностью (за счет владения переменной), но это не исключает гонки. Точно также можно в одну переменную писать и читать, пускай и через мьютекс.blind_oracle
02.04.2024 18:49+2Точно также можно в одну переменную писать и читать, пускай и через мьютекс.
Смотря что считать гонкой. Corrupt Shared State получить уже невозможно т.к. мютекс или другой какой атомик - и то хорошо. В плюсах это UB:
Critical race conditions cause invalid execution and software bugs. Critical race conditions often happen when the processes or threads depend on some shared state. Operations upon shared states are done in critical sections that must be mutually exclusive. Failure to obey this rule can corrupt the shared state.
A data race is a type of race condition. Data races are important parts of various formal memory models. The memory model defined in the C11 and C++11 standards specify that a C or C++ program containing a data race has undefined behavior.[3][4]
(c) https://en.wikipedia.org/wiki/Race_condition#In_software
А логические гонки - ну, это уже на совести погромиста, тут язык мешать не должен как мне кажется.
SabMakc
02.04.2024 18:49+1Не невозможно. Не забываем про unsafe в Rust. Сложнее - да. Но не невозможно.
Кроме того, встречаются либы, которые просто обертки над C-либами. Что тоже не спасет от Corrupt Shared State. Впрочем, это тоже можно отнести к unsafe.
А вот свой код, особенно в рамках safe Rust - да, спасает от подобного. И это очень и очень круто. Потому как дебажить подобное очень сложно.
Lex98 Автор
02.04.2024 18:49+2Гонка данных и состояние гонки это две разные проблемы, они даже подклассами друг друга не являются. Цитата из The Rustonomicon:
Safe Rust guarantees an absence of data races, which are defined as:
- two or more threads concurrently accessing a location of memory
- one or more of them is a write
- one or more of them is unsynchronized
A data race has Undefined Behavior, and is therefore impossible to perform in Safe Rust. Data races are mostly prevented through Rust's ownership system: it's impossible to alias a mutable reference, so it's impossible to perform a data race. Interior mutability makes this more complicated, which is largely why we have the Send and Sync traits.
However Rust does not prevent general race conditions.
Gorthauer87
02.04.2024 18:49Он строго гарантирует отсутствие гонок данных, но не может дать гарантии отсутствия гонок условий.
SabMakc
02.04.2024 18:49Не забываем про unsafe в Rust. Для unsafe никаких подобных гарантий нет.
shares-caisson
02.04.2024 18:49+6Для unsafe подобные гарантии обязан обеспечивать разработчик (т.е. ситуация не хуже, чем в других языках).
SabMakc
02.04.2024 18:49Да, не хуже, чем в других языках, ни капли не спорю.
Просто не надо забывать, что даже если напрямую не используешь unsafe - то твои зависмости вполне могут unsafe использовать.shares-caisson
02.04.2024 18:49+3Вот и ответ на вопрос "почему сторонники активно пытаются всё переписать на Rust?".
SabMakc
02.04.2024 18:49Тогда всю ОС надо на Rust писать и как-то гарантиями безопасности учиться делиться между разными приложениями.
Gorthauer87
02.04.2024 18:49А ещё бывают баги в компиляторе.
SabMakc
02.04.2024 18:49Баги бывают во всех компиляторах.
И с Rust пока встречал баги компилятора пока только в ночных сборках (с WASM было связано).
В Golang находил багу в релизной версии (справедливости ради - то была бага в LSP, а не непосредственно в компиляторе).
Lex98 Автор
02.04.2024 18:49+2И кстати, если вас хоть немного заинтересовал Rust, то еще раз рекомендую лекции Алексея Кладова. Там всего 13 лекций по полтора часа, но этого с головой хватит, чтобы понять что, как, зачем и почему в Rust. Особенно по сравнению с моими мыслями, которые я кое-как собрал в статью.
MountainGoat
02.04.2024 18:49+6Зачем прямо сразу переписывать существующие библиотеки на Раст, если можно сделать обёртку и подтянуть как есть? SQLite, Lua, curl спокойно подключаются, imgui и openimage переписаны "по мотивам". Qt только не будет, слишком другой менталитет.
IkaR49
02.04.2024 18:49+1Qt только не будет, слишком другой менталитет.
Технически, биндинги к Qt - есть... Но я никому не пожелаю с этим работать)
event1
02.04.2024 18:49Мне не нужен язык, мне нужно программу делать, а для этого нужны библиотеки, очень много библиотек,
По этой логике мы все до сих должны были бы писать на фортране. Однако же, не смотря на объём существующих библиотек, языки развиваются и появляются новые. Вполне естественно, что молодые языки беднее библиотеками, чем старые. Тем не менее новые языки набирают популярность и вытесняют старые, а библиотеки переписываются. Для джавы, вон, даже гит свой написали ("Отец, прости им, ибо они не ведают, что творят.") Учитывая, что в русте можно писать обёртки вокруг готовых библиотек, то ситуация даже проще.
syrus_the_virus
02.04.2024 18:49Чем больше я читаю такие хвалебные оды расту и его преимуществам, тем сильнее у меня складывается ощущение, что фанаты раста, критикуя с и с++ за его возможность прострелить ногу, перестают понимать одну вещь - синтаксис с и с++ выстроен из неких математических абстракций вокруг архитектуры ЭВМ, он хоть и даёт возможность забыть про регистры, стек, прерывания и проч., но при этом он позволяет легко об этом вспомнить. Управление памятью, подход к разработке, определяемый компилятором - это всё наверное прекрасно, но кто уверен, что компилятор не ошибается? Ах,да, его пишут боги программирования, они никогда не ошибаются, ведь они пишут язык и компилятор, который никогда не ошибается. Возможно я сильно ошибаюсь, но разрабы, пишущие на расте, скорее всего не понимают, что там под капотом происходит, ведь "у нас самый надёжный супер пупер компилятор,который генерирует самый надёжный и безошибочный код". А потом окажется, через пять лет, что в компиляторе была дыра, которая генерировала код, в котором присутствовали сигнатуры, благодаря которым хакеры могли легко его реверсить и делать эксплойты. Да не, бред какой-то, это же самый надёжный и безопасный язык. Именно благодаря тому, что я на своих плюсах могу стрелять в ногу, и регулярно это делаю, я знаю как сделать так, чтобы мне никто другой не смог выстрелить в ногу. Ничто не совершенно, никто не совершенен.
andreymal
02.04.2024 18:49+11Дыру в Rust-компиляторе найдут и исправят, фикс автоматически применится для всех Rust-программ, собранных новым компилятором.
Миллионы выходов за пределы массива, use-after-free и прочих UB в миллионах сишных программ не найдут и не исправят никогда.
shares-caisson
02.04.2024 18:49+4могли легко его реверсить и делать эксплойты
Да вперед, большая часть кода, где "делают эксплойты" вообще в опенсорсе.
Lex98 Автор
02.04.2024 18:49+4разрабы, пишущие на расте, скорее всего не понимают, что там под капотом происходит
Так в этом и плюс Rust,ты можешь позволить себе не знать, что там происходит (и использовать Rust как Python 4) и при этом писать быстрый и корректный код.
Gordon01
02.04.2024 18:49+6синтаксис с и с++ выстроен из неких математических абстракций вокруг архитектуры ЭВМ
Вокруг PDP-11, если быть точным
он хоть и даёт возможность забыть про регистры, стек, прерывания и проч., но при этом он позволяет легко об этом вспомнить
Зачем мне вспоминать про архитектуру PDP-11? Какое отношение она имеет к современному аппаратному обеспечению c многоядерными процессорами, алгоритмами когерентности кэша, спекулятивному выполнению и тому подобному?
Спойлер - никакого.
но разрабы, пишущие на расте, скорее всего не понимают, что там под капотом происходит
А вы понимаете что происходит под капотом у процессора, который еще раз "компилирует" инструкции для него в микрооперации?
А потом окажется, через пять лет, что в компиляторе была дыра
А потом окажется, через пять лет, что современный процессор - это не одноядерный PDP-11, во что свято верили поклонники мантры "си - это низкий уровень" и произойдет Spectre & Meltdown. Oh shi******
C Is Not a Low-level Language, your computer is not a fast PDP-11
Lex98 Автор
02.04.2024 18:49+4В safe Rust UB нет (точнее, если оно есть то это баг компилятора, который надо исправить), так что мне с практической точки зрения сказать нечего. Из моего теоретического опыта UB действительно очень плохо.
Насколько я понимаю, UB - это когда компилятор считает, что у него есть какие-то гарантии, которые в реальности не выполняются. С UB компилятор может сделать все что угодно, т.к. он будет оптимизировать код исходя из неверных предпосылок. Из-за этого компилятор может, например, просто скомпилировать программу, которая делает не то, что написано в коде или удалить кусок кода с UB, т.к. в правильной программе UB быть не может, а значит этот код недостижим.
UB одновременно зависит от всего вокруг (от кода вокруг, от компилятора, от версии компилятора, от багов в компиляторе, от фазы луны и т.д.), а в другой стороны, если в коде есть UB то эта программа - не программа. Компилятор имеет право сделать абсолютно что угодно со всем кодом т.к. код просто невалиден. Вот статья с примерами UB. Я после этого даже смотреть на языки с UB не могу, там надо прикладывать какие-то невероятные усилия для правильной работы конечного продукта.RichardMerlock
02.04.2024 18:49Всё УБ от оптимизаций на предположениях. С отключением оптимизации будет выполняться то, что написано.
Lex98 Автор
02.04.2024 18:49+5Всё УБ от оптимизаций на предположениях
Не готов ни подтвердить, ни опровергнуть, моих компетенций тут недостаточно. Но писать на C или C++ чтобы потом код без оптимизаций компилировать вряд ли кто-то будет. Проще сразу взять более медленный, но безопасный язык.
RichardMerlock
02.04.2024 18:49Ну вот я пишу без оптимизации на C89 для промавтоматики и мне важно чтобы выполнялось всё как написано в коде. Как раз и медленно и безопасно!
mayorovp
02.04.2024 18:49+10А зачем вообще выбран Си, если нужно "медленно и безопасно"? Там и кроме UB ногострелов достаточно же...
UncleSam27
02.04.2024 18:49+3Потому, что правильная программа на языке С делает только то что ей говорят, и только тогда когда ей говорят. У нее например не запускется сборщик мусора в момент когда тебе нужно обработать критические данные и нужна максимальная производительность.
Написание программ на ассемблере например, это не просто возможность выстрелить себе в колено, это попытка не попасть себе в ногу при каждом шаге, но его продолжают использовать, пишут на нем критически важные участки кода, потому, что ни что не дает большего контроля над железом. А Си наверное самый близкий к железу высокоуровневый язык.
Я понимаю за что современные прикладные программисты не любят С, слишком сложно, слишком много вещей надо держать в голове и не забывать контролировать, это тяжело.
Это просто не их инструмент, оставьте его системщикам, они лучше знают что им надо. Язык Cи в системном программировании "заменяют" последние лет 25 кажется, а он жив до сих пор, по моему мнению только потому, что в этой области удобнее ничего не создали. Безопаснее возможно да, удобней нет.
shares-caisson
02.04.2024 18:49+10Так вот у Раста и цель, чтобы сделать язык, на котором можно писать программы, которые делают только то, что им говорят, только ещё без (многих из) острых углов С.
mayorovp
02.04.2024 18:49+3Потому, что программа на языке С делает только то что ей говорят, и только тогда когда ей говорят.
Ага, и честно выходит за границу массива при ошибке в индексе. Ну прямо то что нужно в промавтоматике - молча перетирать соседние переменные при ошибках в программе.
У нее например не запускется сборщик мусора в момент когда тебе нужно обработать критические данные и нужна максимальная производительность.
Существуют куда более безопасные языки без сборщика мусора или с отключаемым сборщиком.
Это просто не их инструмент, оставьте его системщикам, они лучше знают что им надо.
Вот именно, пусть промавтоматчики и оставят этот язык системщикам, а сами перейдут на что-нибудь более безопасное. Скажем, Rust если нужна производительность, или на какой-нибудь Python если она не нужна. Да и вообще у них даже свои собственные языки программирования есть, хотя к ним у меня отдельные претензии.
SpiderEkb
02.04.2024 18:49Ага, и честно выходит за границу массива при ошибке в индексе. Ну прямо то что нужно в промавтоматике - молча перетирать соседние переменные при ошибках в программе.
Мне почему-то казалось, что язык не должен исполнять роль нянечки в детсаду - ходить за каждым ребенком с платочком и вытирать ему сопли.
Или цель разработки новых языков в том, чтобы всемерно снизить порог входа, чтобы
любая кухарка могла управлять государствомлюбой прослушавший вполуха какой-нибудь инфоцыганский курс, как можно быстрее стал "суперсеньором"?Если в индексе ошибка - оно все равно выстрелит. Не здесь, так в другом месте. Не сейчас, так потом. А так оно просто маскируется языком и вроде как все в порядке.
А если говорить о "безопасности", то решается это на системном, а не языковом уровне - любой объект в системе (а переменная - это тоже объект). должен иметь "операционный дескриптор", который в т.ч. содержит его размер. И любое некорректное обращение будет вызывать системное исключение "выход за границу объекта". В любом языке.
unC0Rr
02.04.2024 18:49+9Мне почему-то казалось, что язык не должен исполнять роль нянечки в
детсаду - ходить за каждым ребенком с платочком и вытирать ему соплиЛюди, даже имеющие огромный опыт, ошибаются. Особенно легко ошибиться при рефакторинге. Если какие-то свойства программы можно проверить автоматически - лучше это сделать автоматически, и чем раньше, тем меньше стоимость ошибки. Именно поэтому даже в традиционно динамических Python и JS (в виде TypeScript) добавляют аннотации типов. Именно поэтому в Расте существует safe подмножество языка, в котором можно спокойно мешать код и не бояться о тысячах способов получить UB. Именно поэтому индустрия стремится отойти от языков со слабой типизацией навроде Си.
Если в индексе ошибка - оно все равно выстрелит. Не здесь, так в другом
месте. Не сейчас, так потом. А так оно просто маскируется языком и вроде
как все в порядке.Отличное описание Си, но что же хорошего в этой маскировке?
И любое некорректное обращение будет вызывать системное исключение "выход за границу объекта"
К сожалению, мы находимся не в идеальном мире, и самые популярные архитектуры не содержат необходимых механизмов для реализации этого. Кроме того, подобные проверки решают только ограниченное количество возможных проблем доступа к памяти.
SpiderEkb
02.04.2024 18:49Люди, даже имеющие огромный опыт, ошибаются. Особенно легко ошибиться при рефакторинге. Если какие-то свойства программы можно проверить автоматически - лучше это сделать автоматически, и чем раньше, тем меньше стоимость ошибки. Именно поэтому даже в традиционно динамических Python и JS (в виде TypeScript) добавляют аннотации типов. Именно поэтому в Расте существует safe подмножество языка, в котором можно спокойно мешать код и не бояться о тысячах способов получить UB. Именно поэтому индустрия стремится отойти от языков со слабой типизацией навроде Си.
Я полностью согласен. Но... Если вы начнете мешать код без риска получить UB, то рано-поздно вы все равно обманете компилятор и потом долго будет думать - почему же оно работает не так ка положено, хотя никаких ошибок тут нет ни при компиляции ни при выполнении?
Или нет таких трудностей, которые мы не смогли бы себе создать. За 30+ лет в разработке в этом убедился многократно.
Отличное описание Си, но что же хорошего в этой маскировке?
Ничего. Кроме того, что любой разработчик, который понимает как оно работает внутри, даже без отладчика вам сразу скажет в чем проблема. Просто "вполглаза" глянув на код.
А вот человек, который привык во всем полгаться на язык и компилятор - да. Будет долго чесать репу.
Спросите любого хирурга - что он предпочтет - скальпель, которым можно порезаться, или пластиковый ножичек из одноразового набора для пикника, который в этом плане абсолютно безопасен.
В целом я не поддерживаю что-то одно, но считаю что каждому овощу свой фрукт. И для каждой цели свой инструмент. И нет абсолютно лучшего и универсального на все случи жизни.
Но если вы взялись на инструмент - изучите все правила безопасной работы с ним и привыкнете их соблюдать. Или, если это непосильно, делайте то, что не требует такого сложного и опасного инструмента.
К сожалению, мы находимся не в идеальном мире, и самые популярные архитектуры не содержат необходимых механизмов для реализации этого. Кроме того, подобные проверки решают только ограниченное количество возможных проблем доступа к памяти.
Ну лично я нашел себе идеальный кусочек, где это есть. Может быть не в полном объеме, но в значительной степени оно так. :-)
UranusExplorer
02.04.2024 18:49+3Ничего. Кроме того, что любой разработчик, который понимает как оно работает внутри, даже без отладчика вам сразу скажет в чем проблема. Просто "вполглаза" глянув на код.
Это только для простых случаев, типа выхода за границы массивов. А если там какое-нибудь совсем не очевидное UB, то даже с отладчиком можно просидеть много часов. Например, потому что под отладчиком и в дебажном билде все отлично работает, а без отладчика и в релизом билде уже нет.
isadora-6th
02.04.2024 18:49Тут на хабре какой-то человек носил свою проблему на Fortran где он сравнивал float-ы через == и ожидал, что условия выполнятся, ведь операции и значения при расчетах одинаковые, однако же, стреляло, что не равно, когда он убирал логирование. УБ однако)
Суть была в том, что в памяти лежал 32битный флоат, а правое значение лежало в регистре 40битном, и они естественно были не равны, а при походах в логи, 40бит регистр складывал 32бита себя в стек, и поэтому работало.
А вообще эта помешанность на корректности меня сильно смущает. Число тупых логических ошибок смело 100 к 1 относительно колдунских проблем языка, где опять же не прав был я.
shares-caisson
02.04.2024 18:49Зато это соотношение меняется на 70 к 30, когда кто-то активно ищет колдунские проблемы в вашем приложении.
mayorovp
02.04.2024 18:49+1Дело не в нянечках и соплях, а в возможности сосредоточить внимание на действительно важных вещах.
А если говорить о "безопасности", то решается это на системном, а не языковом уровне - любой объект в системе (а переменная - это тоже объект). должен иметь "операционный дескриптор", который в т.ч. содержит его размер. И любое некорректное обращение будет вызывать системное исключение "выход за границу объекта". В любом языке.
И после этого вы ещё заявляете о вреде проверок границ, которые приходится отключать в тяжёлых расчётах?!..
Да, решить проблему на уровне платформы - идея хорошая. Только вот в чём проблема - на системном уровне этого не сделать, тут нужна аппаратная поддержка. И неслабая, поскольку хранение длины в указателе приводит минимум к удвоению его размера - а значит, для сохранения производительности нужно удваивать шину данных.
Lex98 Автор
02.04.2024 18:49+5язык не должен исполнять роль нянечки в детсаду
Практика (которая критерий истины) показывает, что писать безопасно на C и/или C++ не может никто. Rust позволяет людям с меньшим опытом писать настолько же быстрые и гораздо более безопасный (уменьшение количества уязвимостей в 70 раз это слишком хорошо, чтобы от этого отказываться).
любой объект в системе должен иметь "операционный дескриптор"
Только систем таких нет. В Rust как раз по умолчанию все это и проверяется (как на этапе компиляции, так и в рантайме).
shares-caisson
02.04.2024 18:49+2А потом говорят, что сообщество Rust токсичное. Да, токсичное тем, что признаёт человеческие слабости и пытается ради общего блага переложить заботу о технических тонкостях на машину. Не [только] для того, чтобы уменьшить порог входа, а для того, чтобы помочь и опытным разработчикам произвести больше полезной работы.
Вы всё ещё можете отключить "нянечку в детсаду", но только при этом вам придётся подумать -- а почему это мне приходится так делать? Обосновано ли это? Не подкладываю ли я себе свинью на будущее? А потом обклеить опасный участок яркой лентой и поставить табличку "осторожно, скользкий пол", чтобы в будущем было проще искать такие места.
firehacker
02.04.2024 18:49А если говорить о "безопасности", то решается это на системном, а не языковом уровне - любой объект в системе (а переменная - это тоже объект). должен иметь "операционный дескриптор", который в т.ч. содержит его размер. И любое некорректное обращение будет вызывать системное исключение "выход за границу объекта". В любом языке.
Что характерно, защищённый режим x86 как раз про это. Но никто не заценил фишку, не воспользовался ей, не создал ни ЯП, ни ОС, которые бы использовали это в полной мере. А затем и Intel забросила и похоронила концепцию.
А до счастья оставался маленький шаг: сделать PDBR не частью CR3, а частью дескриптора сегмента, чтобы проблема фрагментации линейного адресного пространства перестала быть актуальной.
И была бы аппаратная защита от выхода за границы массивом и буферов. Buffer overflow attack не было бы как класса. Атак с шеллкодам не было бы как класса.
SpiderEkb
02.04.2024 18:49Я немного про другое.
Вот, например, получили вы некоторый указатель аргументов функции. И представьте, что у вас есть некое системное API которое позволяет получить информацию о том, на что этот указатель указывает - тип объекта, размер памяти, который он занимает... Т.е. к каждому объекту есть еще "операционный дескриптор" (operational descriptor, opdesc) по которому можно получить некоторую интересную информацию.
Единственное - чтобы это использовать, нужно я вно указать компилятору что требуется передача операционных дескрипторов в функцию. Для С/С++ это будет
int func (char *, int *, int, char *, ...); /* prototype */ #pragma descriptor (void func ("", void, void, ""))
В данном случае для аргументов char* внутри функции всегда можем посмотреть - а что на самом деле за ними кроется - какого размера буфер?
В других языках иначе - в некоторых просто достаточно модификатора OpDesc в прототипе функции.
firehacker
02.04.2024 18:49+1Ваш #pragma descriptor требует пояснения: я не понял, почему в прототипе int (тип возврата), а в прагме void, что означают пара пустых строковых литералов?
И ещё: в вашей концепции запрашивать информацию о размере буфера это обязанность программиста или это неявно будет делать компилятор?
Drucocu
02.04.2024 18:49+5Мне почему-то казалось, что язык не должен исполнять роль нянечки в детсаду - ходить за каждым ребенком с платочком и вытирать ему сопли.
Вы теперь мой любимый комментатор на Хабре: ходите по всем статьям, где упомянут C/C++, и вбрасываете порцию токсичности. Через пару комментариев не забудете упомянуть, что вы консультант по разработке и должность придумали специально для вас. А также, что вы каждый день работаете с кодом, который не правился 10 лет (видимо, на него смотрите) и ещё пару раз оскорбите программистов, пишущих на чём-то, кроме вашего любимого языка.
Так и день пройдёт)
SpiderEkb
02.04.2024 18:49Где вы увидели токсичность в адрес С/С++?
Написано ровно то, что написано. И ничего более. Додумать можно все что угодно, но это уже из разряда "сама придумала - сама обиделась".
К С у меня вполне себе нежные чувства при всех его недостатках. Да, это "опасный" инструмент. Но это уже из серии про дурака и стеклянный йенг.
С С++ сложнее - мне он очень нравился на заре, когда был в варианте "ОО-расширения С". Сейчас, как мне кажется, было бы правильно полностью абстрагироваться от старого наследия и развивать его как несовместимый с С отдельный язык (каковым, по сути, он уже и является). Ну и не переусложнять, пытаясь втащить туда все подряд, а сосредоточится на устранении разного рода UB (раз уж так сейчас модно "безопасно" пользоваться).
Но в целом никаких предубеждений к нему нет. Язык как язык. Не хуже и не лучше (по совокупности характеристик) многих прочих в широком классе задач (но не любых задач).
Есть некоторая предвзятость в людям, которые начинают с пеной у рта доказывать что вот они нашли язык, который самый лучший и самый универсальный для всего на свете. Такого точно не бывает. Можно найти лучший инструмент для конкретной задачи, но не для всех сразу. Получится то ли нейрохирург с бензопилой, то ли лесоруб со скальпелем. Хотя в своих областях и скальпель и бензопила работают отлично.
Drucocu
02.04.2024 18:49+6Где вы увидели токсичность в адрес С/С++?
Вы невнимательно прочитали: я как раз написал, что ваша токсичность направлена на людей, не пишущих на вашем любимом языке. Можно вспомнить, как вы пренебрежительно относитесь к "перекладывателям джейсонов")
Есть некоторая предвзятость к людям
Именно.
начинают с пеной у рта доказывать
Никакой пены у рта у автора не замечаю. Человек делится своими впечатлениями, притом честно даёт представление о своих компетенциях, чтобы мы могли адекватно оценить подаваемый материал. Он же не пишет: "я 20 лет писал на C, но в конец задолбался и перехожу на Rust, так как в очередной раз вылетел за границы массива", верно?
А вот у вас, кажется, что-то капает. Возможно, кровожадная слюна: жаждете выпить из молодых специалистов всю радость от профессии.
RichardMerlock
02.04.2024 18:49B&R любит С. Да и всякие PC-based тоже. Но при наличии культуры кода, руководств, правильных ограничений и отсутствия всяких "смотри как я могу", получается легко читаемый структурированный код.
UranusExplorer
02.04.2024 18:49+2В таком случае вы ходите по минному полю. Эффекты от UB обычно проявляются при включенных оптимизациях, но язык и компиляторы не дают никаких гарантий, что при выключенных оптимизациях эффектов от UB не будет. Если вы действительно таким образом пишете для промавтоматики, то я переживаю за пользователей этой автоматики, очень надеюсь что там не что-то связанное ответственными и опасными применениями.
MountainGoat
02.04.2024 18:49+8Оптимизации никак не помешают сначала удалить указатель, а потом его прочитать. И у вас он будет правильно читаться, а у клиента SEGFAULT. Классика же.
UranusExplorer
02.04.2024 18:49+2Всё УБ от оптимизаций на предположениях. С отключением оптимизации будет выполняться то, что написано.
Покажете параграф в стандарте языка, гарантирующий это?
RichardMerlock
02.04.2024 18:49А покажите параграф в стандарте где должно выполняться что-то иное в противовес к директивам программы?
UranusExplorer
02.04.2024 18:49+4А покажите параграф в стандарте где должно выполняться что-то иное в противовес к директивам программы?
Не "должно", а "может". Параграф 3.64:
Permissible undefined behavior ranges from ignoring the situation completely with unpredictable results, to behaving during translation or program execution in a documented manner characteristic of the environment (with or without the issuance of a diagnostic message), to terminating a translation or execution (with the issuance of a diagnostic message).
То есть прямым текстом стандарт допускает что в случае undefined behaviour может быть совершенно unpredictable результат - в случае UB стандарт разрешает компилятору сгенерировать вообще все что угодно, даже то, чего нет в "директивах программы". Каких-либо исключений из этого правила в случае отключенных оптимизаций компилятора там нет, никаких гарантий не даётся.
А ещё есть замечательный параграф 4.1.2, где сказано
A conforming implementation executing a well-formed program shall produce the same observable behavior as one of the possible executions of the corresponding instance of the abstract machine with the same program and the same input. However, if any such execution contains an undefined operation, this document places no requirement on the implementation executing that program with that input (not even with regard to operations preceding the first undefined operation).
То есть стандарт явно допускает что программа с UB может творить любую хрень (в том числе и то, чего нет в "директивах программы") даже ещё до выполнения того места, где было UB.
RichardMerlock
02.04.2024 18:49Ну так это если вы сами УБ запрограммировали, то и получаете неопределённое. Тут вообще без вопросов! А вот если программировать правильно, то никакого левого поведения не предполагается.
mayorovp
02.04.2024 18:49+5Вы уж определитесь, левое поведение возникает из-за программиста или из-за оптимизаций. Потому что если программировать правильно - то и оптимизации отключать нет смысла.
RichardMerlock
02.04.2024 18:49Конечно из-за оптимизаций! Программист пишет без указателей, мусор не создаёт, память выделяет при старте, границы проверяет.
UranusExplorer
02.04.2024 18:49Ответ неправильный.
Если программист пишет валидный код, не содержащий UB, то стандарт гарантирует, что код будет скомпилирован корректно даже со всеми возможными оптимизациями.
Если программист пишет некорректный код, содержащий UB, то эффекты от UB в коде могут проявиться даже с полностью выключенными оптимизациями.
Всегда можно сказать "нужно программировать правильно, а неправильно программировать не надо", в стандарте C++ описано несколько сотен случаев, вызывающих UB, на почти тысяче страниц. Они далеко не только про память, указатели, мусор и границы, вообще нет. Многие из них совершенно не очевидные (их практически невозможно вывести логически, нужно именно внимательно читать стандарт и знать, что то или иное вызывает UB).
Не существует программистов, писавших в своей жизни сколь-менее сложный код и не допустивших за свою жизни при написании кода ни одной ошибки. Поэтому и про UB так думать не надо, баги вы можете отловить тестированием, а с UB это так просто уже не получится (и статический анализ тоже далеко не факт что отловит все случаи).
UranusExplorer
02.04.2024 18:49+2Например, вы написали вот такой код, когда вам нужно взять быстрый обратный квадратный корень или сконвертировать значение, полученное от внешней железки (там то же самое, но наоборот):
long i; float y; y = number; i = *(long*)&y;
Или сделали то же самое через union, как часто делают:
union myunion { float f; long l; } myunion.f = number; /* тут используете myunion.l */
Казалось бы, все прилично. На вашей платформе размеры long и float равны. У вас один поток. Вы не выделяете память в куче, только на стеке. Вы не выходите ни за какие границы. У вас нет никакого переполнения. У вас нет никакой утечки. Но в Си первый код содержит UB, а в Cи++ и тот и тот код содержат UB. Потому что в первом случае нарушается правило strict aliasing, которое вы обязаны знать, во втором случае нарушается active member rule, которое вы тоже обязаны знать.
Пример с проверкой на переполнение вида if ((X+n) > X) ниже уже разбирался. Если X - signed, то компилятор имеет право выкинуть все такие проверки, заменив их на рандомные true и false в разных частях кода. Вы-то знаете, что для вашего процессора 0x7FFF + 1 = 0x8000, что на вашей платформе будет 32767 и -32768 соответственно, но вот компилятор имеет полное право на это наплевать и скомпилировать что угодно вместо вашего условия. Потому что это не unspecified, а именно undefined behavior.
И подобных приколов, про которые надо именно знать там довольно много.
RichardMerlock
02.04.2024 18:49Ну вот дичь же! Это же точно из серии "смотри как я могу". Можно еще через двойную косвенную адресацию провернуть финт. Но нафига?!
преобразование i = (long)y; разве не покатит чтобы разместить значение в переменной большей разрядности?
Я пишу платформозависимый код, знаю сколько байт мне надо и спокойно пользуюсь типами uint32_t, int64_t и иже с ними.
И зачем вообще заигрывать с адресацией *&var без причины? Байты в слове попереставлять красиво?
Если компилятор заменит проверку if ((X+n) > X) на if (true), то это конечно плохо, лучше бы подставил полный код, хотя я подсчет бы вынес в отдельное выражение и сравнивал уже результаты. Не люблю нагромождения в условиях ветвления.
UPD: хотя, постойте,
i = (long)y;
не покатит. Вы же хотите IEEE 754 float побайтово с мантиссой, экспонентой и всем знаками переместить в long. Но и тут вопрос, а что дальше то? Руками будете парсить, CRC считать? Тогда лучше читать память через memcpy и sizeof.
UranusExplorer
02.04.2024 18:49+2Ну вот дичь же! Это же точно из серии "смотри как я могу". Можно еще через двойную косвенную адресацию провернуть финт. Но нафига?!
преобразование i = (long)y; разве не покатит чтобы разместить значение в переменной большей разрядности?
Нет, не прокатит, оно преобразует число из флота в инт математически, а надо побайтово. Количество байт и там и там одинаковое.
Но и тут вопрос, а что дальше то?
Да я выше уже написал. Например, это нужно для очень популярного алгоритма быстрого расчета обратного квадратного корня.
Или другой вариант, у вас есть библиотечка, которая умеет читать регистры какого внешнего устройства по его специфичному протоколу. Регистры четырехбайтовые, то есть uint32_t, но конкретно в некоторых регистрах в памяти того внешнего устройства лежат float, и вам нужно работать с ним как с float.
Тогда лучше читать память через memcpy и sizeof
Да, это единственно правильный вариант, не вызывающий UB в C ++17 и старше. В C++20 добавили ещё bit_cast. Но программист, не знающий про strict aliasing напишет простой каст как в примере выше и получит UB.
Если компилятор заменит проверку if ((X+n) > X) на if (true),
Ха-ха, нет, все ещё хуже. Компилятор может заменить эту проверку не только на true, но вообще не все что угодно. Вплоть до того, что в одном месте он ее заменит на true, а через несколько строчек в другом месте такую же проверку заменит на false. Вот тут об этом есть.
Понимаете, вот именно в этом и проблема. "Выйдя за пределы буфера при записи вы можете испортить другие данные в рабочем наборе, что может вызвать непредсказуемое поведение программы" - это common sense, это прекрасно понимает любой программист, кто хоть немного имеет представление о том, как хранятся данные в памяти.
А вот "если вы где-то в программе сделаете два указателя разных типов на один и тот же адрес, то согласно стандарту языка компилятор имеет полное право сотворить в генерируемом бинарнике любую дичь даже там, где вы эти указатели не используете" - это уже не так очевидно, для этого нужно именно знать правилах написанные мелким шрифтом в глубинах стандарта языка на тысячу страниц.
SabMakc
02.04.2024 18:49+1С UB встречаешься обычно при смене компилятора / платформы / архитектуры.
Если одним компилятором компилировать под тот Win x64, то шансы встретить UB пускай и есть, но они практически все всплывают еще на этапе тестирования.Пускай опыта с C/C++ у меня совсем немного, но с UB встретился плотно один раз - когда watch дебаггера и реальное исполнение кода давали разные результаты. В итоге я очень долго отлаживал этот код, не понимая в чем проблема.
MinimumLaw
02.04.2024 18:49+10Таки Rust лучше C/C++?
Слушайте, ситуация с Rust напоминает ситуацию с ЛГБТ-повесточкой.
Я совершенно ничего не имею против тех, кто пишет быстрый, надежный и безопасный код на RUST. Молодцы. Но я просто бешусь, когда они начинают рассказывать мне как я должен писать код. Вот и все.
skovoroad
02.04.2024 18:49+13Ситуация действительно как с лгбт-повесточкой, а именно: в оьоих случаях противники путают информацию с агитацией. Каким образом статья на хабре может заставить вас писать иначе? Это же просто информация.
MinimumLaw
02.04.2024 18:49Ну да. Я ведь ровно о том же.
Вообще-то я комментировал не статью. У меня к ней нет никаких претензий. Я отвечал на откровенно провокационный комментарий. И даже процитировал ключевую его часть.
P.S.
И еще - я вместо "противники", писал "молодцы". Что уже само по себе характеризует отношение а оппоненту. Разве нет?
zagayevskiy
02.04.2024 18:49Аргумент одинаковый — если у вас достаточно неокрепший(детский/подростковый/студенческий в случае Rust) ум, то вас легко направить по тому или иному пути. Если вы 40-летний сеньор, то радужный раст вам нестрашон:)
freecoder_xx
02.04.2024 18:49+1Много с чем ситуацию напоминает - "холивар" это называется. Было всегда и будет ещё.
MinimumLaw
02.04.2024 18:49+3Ну да. Лучший инструмент - это тот, которым ты сегодня можешь делать продукт.
А те, кто только начинает вольны выбрать любой из представленных инструментов. В конце-концов всегда "фотографирует фотограф, а не фотоаппарат". И вообще, "Пусть расцветают сто цветов, пусть соперничают сто школ" (с)
domix32
02.04.2024 18:49+1Rust в этом плане просто быстрее внедряет желаемые вещи, пока С++ комитет решает завести новый функционал в стандарт или оставить до новой версии на подумать - в расте оно уже работает в достаточно эргономичном виде. Для примера взгляните на ту же библиотеку ренжей и вью - сколько лет до них добирались и до сих пор нет полного покрытия у всех компиляторов, в то время как в Rust часть синтаксиса языка. Причины понятны, но работать от этого легче не становится. Нормальные модули ждём по сей день.
Rust Embed вполне активен насколько мне известен и опять же эргономика и инструментарий этой экосистемы в среднем лучше чем альтернативы для C++, хотя эквивалентный код вполне можно было написать и на C++. Т.к. работа с платами в любом случае будет сопряжена с манипуляциеми с голой памятью, то на определённых уровнях так или иначе всплывёт unsafe и возможность скрафтить UB, так что с плюсами в этом плане различий меньше. Оно будет просто удобнее в среднем.
datacompboy
02.04.2024 18:49+2наверняка на этот счет есть линтер, но такой код просто не должен компилироваться. А про очевидность поведения zero value для разных типов мне даже говорить не хочется
Вот тут есть чоткое непонимание ситуации. Цель в том, что новое поле всегда может найтись и оно будет нулевым. Приползёт от старого сервиса. Очнётся зомби с кодом недельной давности. Придётся откатить один из сервисов. Надо прочитать файл который создали год назад. И так далее. То есть "новое поле == нечто нулевое" это норма с которой должен работать любой код. Нельзя "не скомпилировать", потому что это нормальное поведение рантайма.
dpytaylo
02.04.2024 18:49+5Если мы поддерживаем старое API, то можем сделать это поле опциональным и обрабатывать надлежающим способом. А вот уже другие ситуации, по моему мнению, стоит рассматривать отдельно, иначе можно получить неприятные ошибки.
MiyuHogosha
02.04.2024 18:49+2И не просто ошибки. Я помню истоии об уязвимостях возникшие именно так. И эксплутируемые именно на основе такого поведения "нулевого поля". По-моему именно из-за этого несмотря на существование "нулевого" поля в "google protobuf", они также ввели и опциональные , и значение по умолчанию, которое "на проводе" кодируется тем же нулем. Т.е. ноль в данных не есть ноль на выходе.
Dair_Targ
02.04.2024 18:49В третьей версии все поля опциональные по-умолчанию. Так что они на эту же проблему нарвались, как, кстати, и другие компании.
Medeyko
02.04.2024 18:49+6Как раз подход с молчаливой инициализацией нулями порождает ошибки, возможности для которых создатели Раста постарались минимизировать. При молчаливой инициализации компилятор не может определить, не забыл ли программист учесть то самое "легаси", про которое Вы говорите.
В Расте же стараются сделать так, чтобы программисту для учёта подобных случаев приходилось явным образом что-то добавить в код, и компилятор, соответственно, может распознать, забыл ли программист учесть то "легаси" или нет.
Приведённые Вами ситуации не означают, что нужна автоматическая инициализация нулями. Они означают, что инициализатор (конструктор), который создаёт структуру из соответствующего источника, должен явным образом обработать эту ситуацию. Если при отсутствии значения соответствующее поле должно инициализироваться нулём - ок, пусть так, но это должно быть явным образом осознанно сделано программистом, а не произойти автоматически. И именно про это говорит автор статьи.
Virviil
02.04.2024 18:49В общем случае это не так и для Раста
#[derive(Default)]
FooBar { foo: 42, ..Default::default() };
И добавление поля так же закончится нулем или что еще хуже -
false
, потому что строка 2 спокойненько скомпилируетсяLex98 Автор
02.04.2024 18:49+3В общем случае это не так и для Раста
Назвать это общим случаем это, конечно, громко сказано.
надо явно для типа указать, что ты хочешь значение по умолчанию и чтобы компилятор вывел его сам из значений по умолчанию полей;
надо явно сказать "я хочу для всех остальных полей значения по умолчанию".
Lex98 Автор
02.04.2024 18:49+10Кроме того, что уже сказано выше про неинициализированные поля мне добавить нечего. Добавить я могу только то, что, по моему мнению, zero values в Golang это ошибка сама по себе. Это заставляет все типы иметь какое-то значение по умолчанию, хотя для типа может просто не быть такого значения.
Каналы хороший пример такого, в nil канале нет никакого смысла, у него нет и не может быть какого-то логичного поведения. Из-за этого теперь у nil канала есть очень странное поведение - создание дедлоков. Зачем вообще иметь возможность создать, по сути, невалидный объект, который при любом обращении вызывает баг?
Или разница в поведении слайсов и мап. Nil слайс на практике от слайса нулевой длины ничего не отличается. Nil мапа же почему-то не может при записи аллоцировать память и теперь у такой мапы есть 2 возможных использования: 1) узнать, что ключа (любого) в этой мапе нет; 2) получить панику при попытке ключ записать. И такой потрясающий объект можно вернуть из функции ничего не подозревающему пользователю, вот он то обрадуется.
Sazonov
02.04.2024 18:49+14Не касательно содержимого статьи, а в целом. Надо понимать что удобство и популярность языка ещё очень сильно зависят от документации и комьюнити. И, может быть субъективно, но у раста очень токсичное комьюнити.
Medeyko
02.04.2024 18:49+4Это довольно неожиданное утверждение. :) Очень грустно его видеть. Обычно напротив говорят, что растовское комьюнити - дружелюбное, гораздо дружелюбнее среднего по индустрии.
А что заставляет Вас считать обратное? Вы лично столкнулись с неадекватной агрессией, когда пришли за советом? Не расскажете подробнее?
Sazonov
02.04.2024 18:49+13Я потому и написал, что это субъективное суждение (меня и некоторых моих друзей). Вполне допускаю что это ошибка выжившего. Я не часто по работе пересекался с Rust программистами, но был один совместный проект в пару лет длиной. Не могу чётко сформулировать свои претензии (оценка токсичности вещь относительная), но большая часть rust программистов с которыми я сталкивался оказывались не в меру высокомерными и слишком большими адептами из разряда «кроме раста другие языки не нужны», «если всё переписать на раст, то мир станет лучше», «вы тупые потому что вы не понимаете прелесть раста», «ваш с++ говно» и так далее, в таком духе.
Lex98 Автор
02.04.2024 18:49+3Вот в чем точно "недостаток" (сами решайте, насколько это действительно недостаток) так это в том, что он заставляет очень критически относится к другим языкам.
После Rust я просто не смог смотреть на Golang, там слишком много вещей, которые должны были бы быть лучше. На C++ я тем более не посмотрю (о чем и в статье написал), для меня у него нет ни одного преимущества перед Rust и огромный товарный состав недостатков.
И да, мир действительно стал бы лучше, если бы все было переписано на Rust, может наконец-то избавились бы от проблем "быстрых" языков и от медлительности всех остальных.
SabMakc
02.04.2024 18:49+2Я вот наоборот, с Golang работаю после Rust (правда c Golang познакомился задолго до Rust).
И с одной стороны - действительно, Rust был гораздо более выразительным и первое время было сложно. А с другой - читать/изучать код стало гораздо легче.Так что не скажу, что после Rust на другие языки смотреть невозможно. Вопрос привычки.
P.S. подобный эффект есть и если после Golang сесть за Java. Java кажется крайне многословной.
Lex98 Автор
02.04.2024 18:49+2У всех свои приоритеты, мне после Rust и про Python сильно долго думать не хочется, в то тоска одолевает. Я понял, что Golang мне абсолютно не нравится когда проходил A Tour of Go и за сколько там упражнений нашел 4 возможности отстрелить себе лишнюю конечность:
возможность читать слайс после длины;
nil map, которая не позволяет делать с собой ничего полезного (хотя nil слайс на практике идентичен слайсу нулевой длины);
поведение nil и закрытых каналов;
возможность переопределять ключевые слова (знаменитое
#define true (rand() > 10) //Happy debugging suckers
).
mayorovp
02.04.2024 18:49Забыли ещё нетривиальное взаимодействие между append и изменением элементов по индексу
Cerberuser
02.04.2024 18:49возможность читать слайс после длины;
Можете пояснить, о чём тут речь? Вроде же, если просто индекс вылезает за границы слайса - мы сразу получаем панику, как и в Rust?
возможность переопределять ключевые слова
Опять же, это, видимо, какой-то такой ногострел, с которым я пока не сталкивался, поясните?
Lex98 Автор
02.04.2024 18:49+3Счастливый вы человек, раз не знаете. Скорее всего после следующего кода настроение у вас ухудшится, так что готовьтесь.
Чтение слайса после длины
package main import "fmt" func main() { s := []int{2, 3, 5, 7, 11, 13} printSlice(s) // Slice the slice to give it zero length. s= s[:0] printSlice(s) // Extend its length. s = s[:6] printSlice(s) s= append(s, 7) printSlice(s) // Works just fine d := s[0:cap(s)][11] fmt.Printf("len=%d cap=%d d=%v\n", len(s), cap(s), d) // Obviously this panics g := s[11] fmt.Println(g) } func printSlice(s []int) { fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s[0:cap(s)]) }
С помощью такой техники можно создавать очень веселые гайзенбаги
package main import ( "fmt" "math/rand/v2" ) type SlidingWindow struct { s []int offset int size int } func (w *SlidingWindow) next() []int { if w.offset > (len(w.s) - w.size + 1) { return nil } t := w.s[w.offset : w.offset+w.size] w.offset += 1 return t } func sliding_window(s []int, offset int, size int) []int { return s[offset : offset+size] } func main() { s2 := []int{1, 2, 3, 4, 5, 6} if rand.IntN(10) > 5 { s2 = append(s2, 7) // panics without append } sw := SlidingWindow{s2, 0, 3} for w := sw.next(); w != nil; w = sw.next() { fmt.Println(w) } }
Переопределение ключевых слов
package main import ( "fmt" "math/rand/v2" ) func really_big_func() (a, b bool) { // a lot of code // Happy debugging, suckers true := rand.IntN(10) > 5 false := rand.IntN(10) > 3 fmt.Println(true) fmt.Println(false) // a lot of code return true, false } func main() { fmt.Println(really_big_func()) }
mayorovp
02.04.2024 18:49+3Погодите, в go операция вырезания куска массива-слайса может этот слайс расширить?! И это не баг, а документированное поведение?
Да какого хрена?!
Кстати, вот пример короче и показательнее
package main import "fmt" func main() { s := []int{1, 2, 3, 4, 5, 6} fmt.Printf("%v\n", s) // [1 2 3 4 5 6] s = s[1:4] fmt.Printf("%v\n", s) // [2 3 4] s = s[1:4] fmt.Printf("%v\n", s) // [3 4 5] }
Lex98 Автор
02.04.2024 18:49Погодите, в go операция вырезания куска массива-слайса может этот слайс расширить?! И это не баг, а документированное поведение?
Не просто документированное поведение, оно прямо в Golang tour описано. И не в том смысле, что не делайте так никогда, а как просто еще одна возможность языка.
Wolfie
02.04.2024 18:49В Go массив != слайс. В данном примере вы можете слайсить массив как угодно, пока вы не выходите за границы массива. Попробуйте в этом же примере создать слайс с индексами, выходящими за пределы массива с данными, получите (справедливо) панику.
Lex98 Автор
02.04.2024 18:49+1Выход за длину это как раз и есть выход за границу слайса и проблема ровно в том, что паники при этом нет, она возникает только при выходе за границу capacity.
mayorovp
02.04.2024 18:49+1Я знаю, что массив - не слайс, но:
во-первых, слайсы играют в Go ту же роль, которую играют массивы в программировании в целом;
во-вторых, в выражение
s[1:4]
синтаксически выглядит как вырезание куска из слайсаs
, а не из скрытого буфера за ним.
Cerberuser
02.04.2024 18:49+1...мда, спасибо, первое действительно проклято. Второе-то ладно, просто кто-то явно забыл про то, как JavaScript прошёлся по граблям с переопределением undefined...
Lex98 Автор
02.04.2024 18:49+3В этом моя проблема с Golang, они там очень много чего "забыли","не знали" или "проигнорировали". Не то, чтобы я считал себя умнее создателей Golang, они явно делали язык под свои конкретные нужды, но когда рандом с улицы (я) может за 5 минут найти дыру в языке, который только начал изучать, то это как-то не очень.
Wolfie
02.04.2024 18:49Да, Go разрешает определять переменные с именами, совпадающими с некоторыми ключевыми словами, или именами импортированных пакетов (это называется shadowing), но любой вменяемый редактор вам это сразу покажет. Так что, что такое "true" в любом случае будет видно сразу.
Слайс -- это тип-надстройка над массивами. Не сами массивы. Это как view таблицы в базе данных.
Lex98 Автор
02.04.2024 18:49Так что, что такое "true" в любом случае будет видно сразу.
Только что проверил,
golangci-lint run
игнорирует такой код (golangci-lint has version 1.55.2
):package main import ( "fmt" "math/rand" ) func main() { true := rand.Intn(10) > 5 if true { fmt.Println("randomly true") } }
Видимо потому, что
true
- слишком незначительная часть языка для того, чтобы true было ключевым словом.Это как view таблицы в базе данных.
Это замечательно, только вот как мне это поможет избежать (это хотя бы можно
s[:3:3]
починить):https://go.dev/play/p/7dkrDMD4v9D
package main import "fmt" func main() { s := []int{2, 3, 5, 7, 11, 13} printSlice(s) s = s[:3] // limiting len printSlice(s) } func printSlice(s []int) { s = s[0:cap(s)] // random func ignores limit fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s) }
Или такой гайзенбаг, когда то, упадет код или тихо будет работать неправильно зависит от того, был ли слайс переалоцирован или нет:
https://go.dev/play/p/Fud-Dmdgq1u
package main import ( "fmt" "math/rand/v2" ) type SlidingWindow struct { s []int offset int size int } func (w *SlidingWindow) next() []int { if w.offset > (len(w.s) - w.size + 1) { return nil } t := w.s[w.offset : w.offset+w.size] w.offset += 1 return t } func sliding_window(s []int, offset int, size int) []int { return s[offset : offset+size] } func main() { s2 := []int{1, 2, 3, 4, 5, 6} if rand.IntN(10) > 5 { s2 = append(s2, 7) // panics without append } sw := SlidingWindow{s2, 0, 3} for w := sw.next(); w != nil; w = sw.next() { fmt.Println(w) } }
Wolfie
02.04.2024 18:49Если слайсы слишком сложно, используйте массивы. Go -- язык простой, позволяет быть продуктивным с минимальным знанием языка.
Lex98 Автор
02.04.2024 18:49+1Если слайсы слишком сложно
Go -- язык простой
позволяет быть продуктивным с минимальным знанием языкаВам бревно в глазу не мешает?
Gorthauer87
02.04.2024 18:49Я бы ещё добавил любовь к "программированию в комментариях". Вот уж просто минное поле.
UncleSam27
02.04.2024 18:49+1Вот за эти высказывания вас и не любят. Скромнее надо быть :)
Lex98 Автор
02.04.2024 18:49+1Просто хочется с людьми поделиться, что огромное количество проблем, с которыми им приходится постоянно сталкиваться уже решена и можно больше не мучиться.
shares-caisson
02.04.2024 18:49+7На несколько сообщений выше сообщество раста уже с геями сравнили за то, что "посмели" рассказывать о преимуществах своего языка. Просто для некоторых людей "токсичное" это обозначает "всё, с чем я не согласен, включая то, что я считаю навязыванием хорошего поведения".
UranusExplorer
02.04.2024 18:49+1Тут скорее зависит не от факта согласия-несогласия, а от того, в какой форме он подается
mayorovp
02.04.2024 18:49+12А с формой-то что не так?
Вот взять обсуждаемый пост. Вроде нормально написан же, но в комментариях всё равно упомянули токсичное сообщество. Получается что ни напиши про Rust - обвинений в токсичности не избежать. Точно ли именно Rust тут токсичен?
UncleSam27
02.04.2024 18:49+1Ну так оно действительно токсичное. Даже автор данного поста допускает заявления типа "мир действительно стал бы лучше, если бы все было переписано на Rust".
Столь радикальные заявления это какой то детский максимализм или рвение неофитов, и у кого то вызывает лишь улыбку, а у кого то сомнения в адекватности этих коллег...
Будет ли специалист по нейросетям, разрабатывать нейросети на rust? Сильно сомневаюсь. В данной задаче язык при помощи которого загружаются данные и выводятся результаты вообще не критичен и тот же Python подойдет лучше, просто за счет того его проще изучить.
Можно ли парсер логов сделать на расте? Да. Он будет быстрым? Да. Будет ли он столь же понятен изящен и лаконичен как на питоне или прости господи perl. Полагаю нет.
Можно ли обработчик прерывания написать на раст? Можно (спасибо Филиппу Опперману). Будет ли он лучше в этом обработчика на С или ASM? Нет.
Думаю смысл вы уловили, есть около 7000 языков программирования каждый создавался для своей области применения, у каждого свои достоинства и свои недостатки, а заявления типа "твой любимый язык программирования отстой, а вот в раст..." ну выше уже вроде написали, что о таких персонажах думают.
shares-caisson
02.04.2024 18:49Будет ли специалист по нейросетям, разрабатывать нейросети на rust?
Работаем потихоньку, вот примеры от самых хайповых компаний индустрии: https://github.com/openai/tiktoken, https://github.com/huggingface/candle, и в целом. Не смотря на все заявления "да питон там только клей для сишных библиотек, он ни на что не влияет" именно питон часто становится ботлнеком, даже при запуске тяжёлых нейронок, вот так да.
Lex98 Автор
02.04.2024 18:49+4Можно ли парсер логов сделать на расте? Да. Он будет быстрым? Да. Будет ли он столь же понятен изящен и лаконичен как на питоне или прости господи perl. Полагаю нет.
Думаю, вы сильно удивитесь, но Rust - это один из лучших языков для написания парсеров. Наличие нормальных енумов и довольно продвинутого pattern matching'а позволяет как очень легко описывать токены, так и легко разделять логику парсера под разные токены.
c0r3dump
02.04.2024 18:49+2Серьёзно? Это и правда очень интересное утверждение. Приведите, пожалуйста, примеры конкретные в сравнении с аналогичными ситуациями в комьюнити других языков? Мне наоборот показалось там все такие няши, что аж не верится и Линуса на них нет)
Lex98 Автор
02.04.2024 18:49+5Не могу говорить за все Rust сообщество, люди там разные и бывает всякое, я точно помню как минимум несколько довольно крупных скандала в Rust комьюнити, но с другой стороны я не знаю ни одного сообщества, где не было бы ни одного скандала вообще.
Обычно, если нужна какая-то помощь, то тут и помогут, и объяснят разные варианты с их плюсами и минусами или почему это плохая идея. В обычном общении я не видел примеров токсичного поведения.
mxr
02.04.2024 18:49+3Абсолютно токсичное по отношению к остальным, сами они этого не видят. Невероятно много контента от различных авторов и комментаторов с +- одним тезисов "А Rust лучше...". Язык может и не плохой и место для него найдется. Но ребята напоминают секту, а цель этой секты весь мир на Rust переписать. А для меня например, т.к я не пишу на rust, некоторые примеры из этой статьи абсолютно не читабельны, хоть и местами смахивает на кресты. Охотно верю, что проблема в отсутствии привычки и наметанного глаза к такому синтаксису. Но многие другие языки мне удавалось читать и понимать без знаний. Например Go отлично читается любым разработчиком знакомым с С подобным языком, почти любая конструкция будет очевидной.
shares-caisson
02.04.2024 18:49+4То, что для одного "токсичность", для другого "энтузиазм". Продвигать и агитировать за хорошие вещи -- это не токсичность, а желание поделиться.
KanuTaH
02.04.2024 18:49+2Тыц, тыц, плюс скрытый (модератором?) комментарий того же гражданина к посту, который мы сейчас обсуждаем - это не "энтузиазм". Трудно сказать, чем конкретно вызвано такое поведение, возможно, это что-то медицинское, но я, когда встречаю очередную мульку про "нетоксичный энтузиазм растаманов", всегда вспоминаю этого гражданина. Как говорится, стоит однажды вы@бать овцу...
Вот, кстати, автор - тоже в своем роде жертва пропаганды. Вот он пишет "На C++ я тем более не посмотрю (о чем и в статье написал), для меня у него нет ни одного преимущества перед Rust и огромный товарный состав недостатков", то есть человек взял и сам себя ограничил, добровольно надел себе шоры. Языка человек не знает (даже C он так и не осилил), но почему-то он уверен, что "у него нет ни одного преимущества перед Rust". Почему? Потому, что ему так кто-то сказал. Воля ваша, но это нездоровый подход.
freecoder_xx
02.04.2024 18:49+1По высказываниям одного человека вы судите обо всём комьюнити? Тем более, что с 22 года Роман уже не является активным участником ru-rust. По-моему вы просто раздуваете на ровном месте.
KanuTaH
02.04.2024 18:49+1По высказываниям одного человека вы судите обо всём комьюнити?
Да если б одного... Просто этот в очередной раз засветился в каментах к статье, которую я читаю, дал повод о себе вспомнить.
Тем более, что с 22 года Роман уже не является активным участником ru-rust.
Извините, не слежу, кто там является активным участником чего. Но если не является - тем лучше для коммьюнити. От таких нужно избавляться. Как говорится, лучше поздно, чем никогда.
По-моему вы просто раздуваете на ровном месте.
А по-моему не на ровном месте. Let's agree to disagree.
mayorovp
02.04.2024 18:49Странно, что в качестве примера токсичности растоманов вы выбрали два случая восхваления С++...
KanuTaH
02.04.2024 18:49+1В качестве примера токсичности я выбрал два случая (на самом деле три), когда пациент просто приходит и начинает поливать клоунами, долбо@бами, и.т.д.
восхваления С++
И что? Типа раст можно "восхвалять", а C++ - нет? Вот за это вас и не любят (С) :)
mayorovp
02.04.2024 18:49Я знаю, что случая три, просто плюсы он "защищал" в двух.
И что? Типа раст можно "восхвалять", а C++ - нет?
Я такого не говорил. Но считать агрессивного фанатика С++ примером токсичности сообщества Rust как-то странно.
KanuTaH
02.04.2024 18:49Вы явно что-то перепутали насчёт "агрессивного фанатика C++" :) Ознакомьтесь с профилем пациента получше.
mayorovp
02.04.2024 18:49Какая разница что у него в профиле? Примеры "токсичности сообщества Rust" менее странными от этого не станут.
KanuTaH
02.04.2024 18:49+1Разница в том, что это "агрессивный фанатик" не C++, а совсем другого языка на букву "рэ". Восхвалением C++ он никогда не занимался, наоборот - он приходил в статьи, где, по его мнению, занимались "восхвалением C++", и гадил там. Я не просто так порекомендовал вам ознакомиться с его профилем.
Lex98 Автор
02.04.2024 18:49+2Языка человек не знает (даже C он так и не осилил), но почему-то он уверен, что "у него нет ни одного преимущества перед Rust". Почему? Потому, что ему так кто-то сказал. Воля ваша, но это нездоровый подход.
А еще мне в школе "кто-то сказал" про правила русского языка, математику с геометрией (доказательства теорем, конечно, тоже давали, но что-то мне подсказывает, что никто из учеников не заметил бы ошибки в доказательстве), физику, химию и много чего еще. Жизнь у меня одна и проверить все самостоятельно невозможно. У C есть объективные проблемы, отрицать этот факт бессмысленно.
Мне бы хотелось больше вопросов по существу вопроса, а не по форме. Если я где-то не прав, вы не стесняйтесь, напишите. Если у вас будут хорошие аргументы я вам обещаю, что изменю свое отношение к C и C++.
Gorthauer87
02.04.2024 18:49Вопрос у меня такой, вот вы обвиняете Rust сообщество в токсичности, но тут же сами же в тексте используете агрессивные обороты типа "мулька", "что-то медицинское" или "вы@бать овцу".
Нет ли в этом противоречия?
KanuTaH
02.04.2024 18:49А у меня другой вопрос - даже если предположить(!), что вы правы, и обороты "мулька" и т.п. являются, по вашему мнению, "агрессивными" (хотя по моему мнению это сильно напоминает попытку докопаться до столба), как это оправдывает поведение пациента по вышеприведенным ссылкам? :)
zartdinov
02.04.2024 18:49+10Мне кажется, перебор с символами пунктуации в синтаксисе языка: .|&*::-><(),_>>()?;
Leopotam
02.04.2024 18:49Это в коде еще lifetime-ов не было, количество знаков пунктуации удвоилось бы.
Medeyko
02.04.2024 18:49+2Существенная доля этих символов - желание сохранить определённую преемственность от C/C++, чтобы облегчить мало знакомым с Растом людям понимание кода.
Но важнее, пожалуй, понять какие есть альтернативы? Использование ключевых слов удлинило бы программу, сделав её набор медленнее, а текст менее наглядным. Вот взять тот же самый "?" - это же синтаксический сахар, который заменяет достаточно длинный код, делающий корректную обработку ошибок существенно менее напряжной!
От круглых скобок в if'е избавились - в отличие от многих языков программирования, они в Расте не обязательны (т.е. "if x > 0", а не "if ( x > 0 )" ).
От фигурных скобок можно было бы избавиться методом Питона.
От круглых скобок в функциях одного аргумента теоретически можно попробовать избавиться, но если аргументов более одного?Мне очень интересно было бы увидеть подходы, за счёт которых можно было бы избавиться от лишних символов, одновременно увеличив выразительность кода!
Mabu
02.04.2024 18:49Использование ключевых слов удлинило бы программу, сделав её набор медленнее
Я очень надеюсь, что вы с пользой потратили эти сэкономленные 100 миллисекунд?
me21
02.04.2024 18:49Мне в этом смысле нравится Питон. Код читается почти как просто английский текст.
Lex98 Автор
02.04.2024 18:49+3Меня не особо волнует синтаксис как таковой, не Brainfuck и ладно. К любому синтаксису привыкаешь за 2 недели. Мне гораздо важнее то, что с помощью этого синтаксиса можно сделать.
SabMakc
02.04.2024 18:49+3Появилось впечатление, что Rust сравнивался в 1ю очередь с Python. И после Python он оставил приятные впечатления.
В целом я могу согласиться, что Rust оставляет приятные впечатления. Своим синтаксисом, своими возможностями, своими абстракциями... Но это справедливо для небольших проектов. Для большого я бы его не брал. На Rust очень сложно писать - постоянно приходится бороться с компилятором. Но читать готовый код - еще сложнее. Макросы, сложный код, не всегда можно найти реализацию конкретного метода. Далеко не все библиотеки хорошего качества, что усложняет работу и так с непростым инструментом.
Лично я для большого проекта брал бы язык с более очевидным синтаксисом (и пускай с не такими чистыми абстракциями). Например, тот же Golang.
P.S. Да, статью "что не так с Golang" я бы почитал.
Lex98 Автор
02.04.2024 18:49+3Но это справедливо для небольших проектов.
Как раз наоборот, Rust в полной мере раскрывается в больших, сложных или долгих проектах! Как раз основная мысль статьи в том, что Rust намного проще рефакторить, что наиболее важно как раз в больших проектах. В Rust компилятор не враг, чтобы с ним бороться, это лучший друг, которых покажет на ошибку на самом раннем этапе разработки.
К сожалению, у меня нет личного опыта подобных проектов в Rust. Мое мнение о Rust основано на опыте больших проектов в других языков, но о больших проектах говорят в Fast Development In Rust, Part One, Beyond Safety and Speed: How Rust Fuels Team Productivity, Grading on a Curve: How Rust can Facilitate New Contributors while Decreasing Vulnerabilities и нескольких реддит постах из начала статьи, так что мне кажется, что моя экстраполяция вполне валидна.
Лично я для большого проекта брал бы язык с более очевидным синтаксисом
Выше я уже отвечал, для меня синтаксис не играет большой роли. Rust позволяет мне выражать мои идеи в коде без необходимости неделю думать о UB, том, что надо создать тип Nothing чтобы отделять разные типы пустых значений и т.д.
Да, статью "что не так с Golang" я бы почитал.
Это скорее шутка, у меня, все же, слишком мало опыта в Golang, буквально 2 недели попытки его выучить и пара месяцев обсуждений с друзьями "как так оказалось, что в Golang все настолько плохо". Просто когда в официальном курсе на официальном сайте Golang я умудрился задуматься "хм, кажется это какой-то бред" (и при этом я оказывался прав) раза 4.
Но уже много кто написал такие статьи, я рекомендую I want off Mr. Golang's Wild Ride, Lies we tell ourselves to keep using Golang, They're called Slices because they have Sharp Edges: Even More Go Pitfalls, Golang is not a good language.
Для примера добавлю сюда примеры кода, который вызывает у меня вопросы в духе "а хоть кто-то при разработке над чем-то кроме GC и async рантайма думал вообще?":
Заголовок спойлера
package main import "fmt" func main() { s := []int{2, 3, 5, 7, 11, 13} printSlice(s) // Slice the slice to give it zero length. s= s[:0] printSlice(s) // Extend its length. s = s[:6] printSlice(s) s= append(s, 7) printSlice(s) // Works just fine d := s[0:cap(s)][11] fmt.Printf("len=%d cap=%d d=%v\n", len(s), cap(s), d) // Obviously this panics g := s[11] fmt.Println(g) } func printSlice(s []int) { fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s[0:cap(s)]) }
package main import ( "fmt" "math/rand/v2" ) type SlidingWindow struct { s []int offset int size int } func (w *SlidingWindow) next() []int { if w.offset > (len(w.s) - w.size + 1) { return nil } t := w.s[w.offset : w.offset+w.size] w.offset += 1 return t } func sliding_window(s []int, offset int, size int) []int { return s[offset : offset+size] } func main() { s2 := []int{1, 2, 3, 4, 5, 6} if rand.IntN(10) > 5 { s2 = append(s2, 7) // panics without append } sw := SlidingWindow{s2, 0, 3} for w := sw.next(); w != nil; w = sw.next() { fmt.Println(w) } }
package main import ( "fmt" "math/rand/v2" ) func really_big_func() (a, b bool) { // a lot of code // Happy debugging, suckers true := rand.IntN(10) > 5 false := rand.IntN(10) > 3 fmt.Println(true) fmt.Println(false) // a lot of code return true, false } func main() { fmt.Println(really_big_func()) }
package main type Container struct { a int // old field Items map[string]int32 // new field } func (c *Container) Insert(key string, value int32) { c.Items[key] = value } func main() { c := Container{a: 2} c.Insert("number", 32) }
SabMakc
02.04.2024 18:49Я наоборот слышал, что рефакторить сложно Rust. Особенно если где какой тип поменять надо (или его обертку). Но тут лично за себя отвечать не буду - лично не участвовал в больших рефакторингах c Rust (хотя и очень косвенно наблюдал, и по сложившемуся впечатлению - это было "больно").
Но должен сказать, что рефакторинг - в любом языке не самая простая задача. Хотя он может быть достаточно безболезненен, если код хорошо покрыт тестами.
В Rust компилятор может и друг, но далеко не всегда можно понять "а что же тебе не хватило". И в таких ситуациях лучший подход - запустить пустой проект и там последовательно воспроизвести работающий код, чтобы понять "а почему это работает".
Вопрос с большим проектом не в синтаксисе, а в читаемости кода - чуть ниже расписал. UB и Nothing - не такие уж и большие проблемы на практике, я бы сказал (уточню, что с C/C++ я очень мало работал).
"хм, кажется это какой-то бред" у меня возникал и при чтении Rust book. Но уже не вспомню толком на какие моменты.
mayorovp
02.04.2024 18:49Тип менять больно в любом языке, в котором есть типы. Rust тут, напротив, хорош возможностью заранее "подстелить соломку" в виде псевдонима для типа (type alias) - в таких языках как C# даже это невозможно (и ничего, как-то рефакторим код).
SabMakc
02.04.2024 18:49Честно скажу, этот аргумент именно что слышал в каком-то обсуждении, по существу, из своего опыта, не могу ни подтвердить, ни опровергнуть.
P.S. А рефакторить тип в языках, где нет типов - это еще веселее )))
P.P.S. Да, я знаю что "нет типов" говорить некорректно. Динамическая типизация не исключает типы.
Lex98 Автор
02.04.2024 18:49Я бы даже сказал, что лучше не лениться и для такого использовать паттерн New Type. Это позволяет гарантировать, что невозможно вместо старого типа использовать новый. Да, иногда приходится реализовывать базовые операции над типами заново, но гарантии того стоят.
У меня в моем трассировщике как раз есть такой пример: в оригинальной статье на C++ использовались как раз алиасы для color, vec3 и point3. Я не поддаваться на эту легкость и сделал три независимых типа (заодно узнал, как писать свой derive чтобы не копипастить базовые операции над типами).
Lex98 Автор
02.04.2024 18:49+1Особенно если где какой тип поменять надо
Как раз пример из статьи (где я менял t64 на свою обертку над u8), как мне кажется, должен показать, что это как раз очень просто. Компилятор сразу выдаст все места, где надо поменять тип и его невозможно забыть его поменять - программа просто не скомпилируется. Для Rust компилятор - это как набор встроенных тестов, которые гарантируют, что типы везде сошлись. Конечно, тесты на логику работы лишними точно не будут, но это уже отрезает огромную пачку возможных проблем.
Medeyko
02.04.2024 18:49+12Но это справедливо для небольших проектов. Для большого я бы его не брал.
Вот это высказывание меня тоже несколько удивило... По-моему, Rust как раз в первую очередь предназначен для больших проектов, в нём значительная часть фич именно на большие проекты заточены. "Борьба с компилятором" (которая с накоплением опыта исчезает, кстати), в частности, как раз помогает получить поддерживаемый код, который не "развалится" при росте кодовой базы.
SabMakc
02.04.2024 18:49+1Моя основная претензия к Rust - его сложно читать. Сложно изучать код библиотек, особенно если библиотека не лучшего качества. Сложно погружаться "а что там внутри происходит" при отладке, особенно если там - макрос на макросе и макросом погоняет.
Именно этот фактор ограничивает, на мой взгляд, применение Rust в больших проектах."Борьба с компилятором" - да, со временем становится легче, не спорю. Особенно если к одному проекту более-менее привык и уже знаешь, как с ним работать. Но не могу сказать, что этот фактор пропадает совсем. И я понимаю, что "борьба с компилятором" vs "борьба с плавающими багами" - уж лучше компилятор помучить.
Lex98 Автор
02.04.2024 18:49Про сложность ревью кода в Beyond Safety and Speed: How Rust Fuels Team Productivity говорил Lars Bergstrom. По внутренним опросам в Google android, 50% опрошенных сказали, что код на Rust проще проверять (возможно, что это по сравнению с C++ и тогда это не то, чтобы сильно впечатляюще). Так что это дело просто привычки, вместо поиска скрытого UB можно сконцентрироваться на других вещах.
freecoder_xx
02.04.2024 18:49+3Почему мне не сложно читать код библиотек на Rust? Я профессионально программирую на Rust уже более 5 лет. Но даже в первый год его изучения я прочитал СТОЛЬКО чужого кода в библиотеках, сколько не читал за предыдущие 10 лет. Так что не спешите обобщать свои выводы на всех.
SabMakc
02.04.2024 18:49Не знаю. Может попадалось слишком много библиотек с обилием макросов и кодогенерации. Так-то простой код Rust вполне читаем, вопросов нет.
Может не всегда понятно как именно он работает в деталях (с учетом трейтов и прочей магии шаблонов Rust), но что именно в нем происходит - вполне понятно.
mayorovp
02.04.2024 18:49Я вообще не программировал на Rust всерьёз, но мне тоже не составляет труда прочитать код на этом языке.
1755
02.04.2024 18:49+5Мне кажется, борьба с компилятором – это хорошая возможность избежать проблем в проде. В основном, это ошибки про заимствования.
Я в основном пишу сейчас на Go, но с некоторыми вещами все еще не свыкся (внимание, дальше идет только очень субъективное ощущение):
После Rust - это наличие нулевых указателей. Так что это с большой долей вероятностью вылетит в рантайме и в проде.
Нет разделения mutable/unmutable указателей.
defer решает часть проблем, но не снимает их. Например, легко представить ситуацию, когда взял lock но забыл прописать defer на освобождение.
Лбв к скр им п.
Система импортов кажется топорной, не знаю как лучше объяснить.
Использование первого символа названия для определения публичности.
-
Обработка ошибок очень топорная и ограниченная.
SabMakc
02.04.2024 18:49+1Да, я тоже с Golang работаю после Rust (правда c Golang познакомился задолго до Rust). И со всеми пунктами согласен.
Но с опытом пришло понимание, что "чем проще - тем лучше". И Golang - это именно что простой язык. Он далеко не идеальный, но за счет простоты - я могу ему простить это.
P.S. сильно веселее было раньше, когда у Golang не было еще и менеджера зависимостей )
1755
02.04.2024 18:49Согласен, когда принимаешь "проще - лучше" и заточенность под определенную нишу, то становится жить веселее)
domix32
02.04.2024 18:49Система импортов кажется топорной, не знаю как лучше объяснить.
А как это проявляется?
qalisander
02.04.2024 18:49+1У вас больше вопрос к разработке и опыту с ide, которые подерживают rust все лучше с каждым годом, но все еще не очень по сравнению с Java/Kotlin/C#. JetBrains уже отдельную ide для rust выкатили RustRover но еще в альфа релизе. А rust компилятор выводит довольно хорошие подсказки для новичков. Тем не менее после полугода работы с языком понимаешь сам без помощи компилятора, когда код собирется и как писать нужно т.е становишься продуктивнее и в плане чтения кода тоже. Библиотеки конечно бывают разные, ваша правда) Но потенциал развития довольно большой. Все частоиспользуемые либы вроде сериализаций(serde), веб фреймворков уже довольно хороши. Касательно больших проектов. Большая часть бэкенд сервисов одной крупной криптобиржи написано на rust https://blog.kraken.com/product/engineering/oxidizing-kraken-improving-kraken-infrastructure-using-rust и воочию выглядели неплохо.
SabMakc
02.04.2024 18:49Не знаю на счет RustRover, но раньше их плагин Rust основывался на LSP. И с момента прихода rust-analyzer взамен RLS стало намного лучше.
Но большой разницы с той же VS Code я не увидел - LSP и там и там.
Да, стандартная библиотека, крупные и популярные либы - выглядят очень неплохо (подозреваю, это и было фактором, определившим их популярность).
Опять же, подсказки, в большинстве, своем очень хорошие, спору нет. Но не всегда из них можно понять что именно не так и как это исправить.
После полугода с языком - может некоторое число проблем с компилятором и уходит, но не исчезает полностью. И заранее предсказывать мне так и не удавалось. Возможно тут сыграл фактор, что я работал над множеством небольших и разноплановых проектов (в большинстве своем), а не над одним большим. И к каждому проекту привыкать надо отдельно - а общего навыка под множество либ и их особенностей так и не выработалось за это время.
Большой проект из множества микросервисов - это не тоже самое, один большой монолит )
С микросервисной архитектурой Rust прекрасно справляется, спору нет. Микросервисы писать на Rust - одно удовольствие, могу подтвердить.Не стал уточнять сразу - но в микросервисной архитектуре я Rust вполне вижу. Он действительно очень удобен в таком виде, а этап привыкания и борьбы с компилятором и либами достаточно небольшой.
Правда размер бинарников (debug), объем артефактов сборки (десятки и стони гигов), длительность компиляции (я понял, зачем мне 16 ядер и 64Gb RAM) - могут сыграть не в пользу Rust и тут ))) Но это больше вопрос техники.
Cerberuser
02.04.2024 18:49раньше их плагин Rust основывался на LSP.
У них уже довольно давно своя реализация, не rust-analyzer.
SabMakc
02.04.2024 18:49Возможно.
Я скорее основываюсь на впечатлениях, а не на твердом знании "что там под капотом".
Во времена VS Code с RLS существенной разницы c Rust-плагином я не увидел (возможно совсем небольшое преимущество было за JetBrains - деталей не помню).
Во времена VS Code с rust-analyzer существенной разницы c Rust-плагином я не увидел.В переходный период Rust-плагин JetBrains отставал, как отставал RLS от rust-analyzer.
Но я все возможности досконально не сравнивал - только подсветку типов, да авто-дополнение (то, с чем работаешь больше всего и что можно быстро сравнить). В идеале, конечно, надо было погонять IDE "и в хвост, и в гриву", для формирования более целостного впечатления.И да, это было достаточно давно - как там сейчас обстоят дела я не скажу.
qalisander
02.04.2024 18:49В плане веба большой монолит чаще (но не всегда) считается плохой практикой. Но вот самый большой проект на rust в одном репозитории (не веб), что я знаю это сам компилятор языка. Там по сути много маленьких crates, которые прилинкованы к корневому workspace. Кажется что лепить что-то большое можно, хоть я и не пробовал лично)
В плане "железа" для rust проектов экономить не приходится. Ведь если с нуля, то компилируются все все зависимости.
SabMakc
02.04.2024 18:49"Большой монолит" когда-то был достаточно безальтернативной практикой )
Микросервисный подход получил популярность "всего лишь" лет 10 назад... Лишь с появлением и развитием Docker он стал популярен - когда управлять кучей сервисов стало сильно проще.Впрочем, и сейчас монолит может сильно выиграть в плане производительности - так что его век может еще и вернется...
В плане артефактов сборки - проблема усугубляется тем, что под разные версии компилятора и под разные версии зависимостей - зависимости компилируются в отдельные файлы. Поэтому со временем target только растет. А уж если использовать свежие ночные сборки компилятора...
P.S. а еще есть очень холиварный вопрос - когда микросервис перестает быть микро?
qalisander
02.04.2024 18:49Впрочем, и сейчас монолит может сильно выиграть в плане производительности
Согласен, согласен. Тут общего мнения нет. It depends.
В плане веба большой монолит чаще (но не всегда) считается плохой практикой.
Что бы холивары не разводить, поэтому так и выразился) "Маленький/средний монолит" точно считаю нормой.
когда микросервис перестает быть микро?
А это вечный вопрос)
vdasus
02.04.2024 18:49когда микросервис перестает быть микро?
Когда он становится монолитом?
qalisander
02.04.2024 18:49+2Если вам действительно интересно и вы не хотите просто потролить.
То есть к примеру классификация Amazon где они делят все существующие решения на monolithic, SOA (сервисы) и microservices(микросервисы) и описывают где проходит грань с примерами. Естествено я допускаю что классификаций много и мнения есть другие. По этому это вечный вопрос)
vdasus
02.04.2024 18:49+1Я просто пошутил. А смайлики тут непопулярны. Тут все такие серьезные... Я знаю и что такое микросервисы и [классификации] и имею 35+ лет опыта в IT. Но спасибо что потратили время на меня. Я это ценю. (не сарказм)
mayorovp
02.04.2024 18:49А вот насчёт безальтернативности - не факт. К примеру, в той же WS-Addressing такая штука как Reference Parameters идеально подходит для передачи контекста запроса между микросервисами. Не могли же её придумать с потолка? Значит, нечто похожее на микросервисы писалось, пусть и называлось по-другому.
SabMakc
02.04.2024 18:49Был SOAP, были другие методы и подходы к удаленному вызова процедур (и удаленного выполнения кода в целом).
Сам по себе подход "вызов сервиса из другого сервиса" был известен намного раньше появления термина "микросервисы" и популяризации такой архитектуры.mayorovp
02.04.2024 18:49Для простого RPC достаточно заголовков MessageId и RelatesTo (ну и Action, хотя некоторые библиотеки обходятся без него).
Reference Parameters нужны для хореографии распределённых саг.
Vladicus
02.04.2024 18:49+2Безотносительно Раста, такие статьи, подсознательно относишь как к рекламе Гербалайфа. Слишком хорошо, что бы быть правдой. Автор, конечно же не виноват, но...
Lex98 Автор
02.04.2024 18:49+5Нет причин не проверить это самостоятельно, за Rust ни в какой форме денег не берут)
P.s. если кто-то хочет мне платить за рекламу Rust (хотя бы шоколадными медальками) то пишите в личку!
gev
А как в сравнении с Haskell?
unC0Rr
Отлично, на самом деле. В Haskell что ни говори, тяжело писать большие приложения. Rust берёт же от хаскеля много хорошего, и при этом даёт уверенность, что у тебя не утекают от лени thunk-и, что скомпилированный код скорее всего отлично работает, что твои попытки оптимизации работают (привет, мемоизация и прочая хаскелевская муть). Rust - это быстрый Haskell с императивностью и мутабельностью в нужных местах и по делу.
Gorthauer87
Ну нет, в Расте нет типов высшего порядка
unC0Rr
Да, конечно, раст проще. Но он отличный компромисс для эффективного программирования производительного кода с относительно высокими гарантиями безопасности.
GospodinKolhoznik
Это не на Haskell тяжело писать большие приложения. На Haskell их легко писать за счет того, что язык очень сильно помогает в рефакторинге кода.
unC0Rr
Хаскель отлично помогает своей системой типов, само собой. Но он слишком сложный для большинства программистов. Память из-за ленивости бывает сложно понять куда уходит и что с этим делать. В половине библиотек изобретают операторы, которые делают код компактнее, но приходится учить все эти закорючки, чтобы даже просто читать код.
klvov
это да. я не настоящий функциональщик, но иногда посматриваю в ту сторону. Вот на страничке Control.Lens.Operators перечисляются эти операторы:
123 оператора. Это точно надо их столько, чтобы программировать? Вот и я задаюсь этим риторическим вопросом.
Hidden text
(#%%=)
(#%%~)
(#%=)
(#%~)
(#)
(#=)
(#~)
(%%=)
(%%@=)
(%%@~)
(%%~)
(%=)
(%@=)
(%@~)
(%~)
(&&=)
(&&~)
(&)
(&~)
(=) (~)
(=) (~)
(+=)
(+~)
(-=)
(-~)
(...)
(.=)
(.>)
(.@=)
(.@~)
(.~)
(//=)
(//~)
(<#%=)
(<#%~)
(<#=)
(<#~)
(<%=)
(<%@=)
(<%@~)
(<%~)
(<&&=)
(<&&~)
(<&>)
(<=) (<~)
(<=) (<~)
(<+=)
(<+~)
(<-=)
(<-~)
(<.)
(<.=)
(<.>)
(<.~)
(<//=)
(<//~)
(<<%=)
(<<%@=)
(<<%@~)
(<<%~)
(<<&&=)
(<<&&~)
(<<=) (<<~)
(<<=) (<<~)
(<<+=)
(<<+~)
(<<-=)
(<<-~)
(<<.=)
(<<.~)
(<<//=)
(<<//~)
(<<<>=)
(<<<>~)
(<<>=)
(<<>~)
(<<?=)
(<<?~)
(<<^=)
(<<^^=)
(<<^^~)
(<<^~)
(<<||=)
(<<||~)
(<<~)
(<>)
(<>=)
(<>~)
(<?=)
(<?~)
(<^=)
(<^^=)
(<^^~)
(<^~)
(<|)
(<||=)
(<||~)
(<~)
(?=)
(??)
(?~)
(^#)
(^.)
(^..)
(^=)
(^?!)
(^?)
(^@.)
(^@..)
(^@?!)
(^@?)
(^^=)
(^^~)
(^~)
(|>)
(||=)
(||~)
GospodinKolhoznik
Очень не рекомендую использовать лизны, да и вообще любую оптику в Хаскеле. Там проблема не только в куче странных значков, там ещё и смысл у них у всех зело мудрёный. Да и без них можно рпекрасно обходится (проблема в том, что иногда приходится читать чужой код с линзами и это боль).
Проверено, даже те люди которые сами писали с помощью этих лизн, призм и т.п. уже через пол года сами не могут прочесть и понять собственный код и что там понаписано. С линзами так - пока ты ими ежедневно пользуешься всё супер, стоит сделать небольшой перерыв и они забываются.
У других хаскельных библиотек такой фигни нет. Линзы это что-то действительно мудрёное и быстро забывается. По сути это отдельный язык в языке, со своей уникальной логикой и какими то своими парадигмами. Его отдельно изучают и по нему-же самостоятельные книги пишут.
И вот пример могучей силы оптики из этой книги:
Круто конечно, как ловко и кратко но какой ценой! Это же надо реально изучить, врубиться и постоянно освежать эти знания.
А чтобы удобно работать с многократно вложенными друг в друга структурами данных, вместо линз лучше использовать record dot preprocessor.
RichardMerlock
foreach (var i in array) Console.WriteLine(i ^ 0x02);
mayorovp
Это вы вывели массив. а код выше его модифицирует. Должно быть вот так:
Lex98 Автор
В планах на изучить Haskell есть (может еще F#), но скорее для того, чтобы посмотреть ~~есть ли жизнь на марсе~~ как живется в полностью функциональном языке без доступа к мутабельности, может быть позаимствовать какие-нибудь подходы.
Starl1ght
И там и там 2 вакансии, так что равны +-
qalisander
Кстати нет. Не так давно видел вакансии на Rust в Сбере, МТС и Тиньке помимо мелких компаний и стартапов. Ну и зарубежных удаленных еще больше. И поскольку язык молодой требований из серии 5-10+ лет опыта на нем нет.
GospodinKolhoznik
На Rust вакансий все же раз в 20 больше, чем на Haskell. С другой стороны количество Rust-истов гораздо больше Haskell-истов.
boldape
В моем регионе последние пару лет что я слежу все, я повторю ВСЕ из примерно 2 десятков открытых вакансий в любой момент времени это крипта. 3 года назад мне повезло увидеть 1 вакансию на расте и не крипта.