что написано на rust
Rust — молодой и дерзкий язык программирования
Говорят, что это одновременно C++ и Haskell.
Первая версия языка Rust появилась в 2010 году, и он сразу занял третью строчку в списке любимых языков разработчиков на StackOverflow. Год спустя Rust возглавил этот список и держался там несколько лет. Давайте посмотрим, почему этот язык стал таким популярным, в чём его особенности и почему вокруг него много споров.
В чём идея языка Rust
Автору языка нравилась скорость работы и всемогущество языка C++ и надёжность Haskell. Он поставил перед собой задачу совместить оба этих подхода в одном языке, и за несколько лет он собрал первую версию языка Rust.
Rust позиционируется как компилируемый системный мультипарадигмальный язык высокого уровня. Сейчас поясним, что это значит.
👉 Компилируемый язык означает, что готовая программа — это отдельный файл, который можно запустить на любом компьютере с нужной операционной системой. Для запуска не нужно устанавливать среду разработки и компилятор, достаточно, чтобы скомпилированная версия подходила к вашему компьютеру.
👉 Системный — это когда на языке пишут программы для работы системы в целом. Это могут быть операционные системы, драйверы и служебные утилиты. Обычные программы тоже можно писать на Rust — от калькулятора до системы управления базами данных. Системный язык позволяет писать очень быстрые программы, которые используют все возможности железа.
👉 Мультипарадигмальный значит, что в языке сочетаются несколько парадигм программирования. В случае Rust это ООП, процедурное и функциональное программирование. Причём, ООП в Rust пришло из C++, а функциональное — из Haskell. Программист может сам выбирать, в каком стиле он будет писать код, или совмещать разные подходы в разных элементах программы.
Синтаксис и код
За основу синтаксиса в Rust взят синтаксис из C и C++.Например, классический «Привет, мир!» на Rust выглядит так:
fn main() <
println!(«Hello, world!»);
>
Если вы знакомы с подобным синтаксисом, то сможете быстро начать писать и на Rust. Другое дело, что в Rust есть свои особенности:
let x = if new_game() < 4 >
else if reload() < 3 >
else
Последнее разберём подробно. При такой записи переменная x будет равна четырём, если функция new_game() вернёт значение true. Если этого не случится, компилятор вызовет функцию reload() и проверит, что получилось. Если true, то x примет значение 3, а если и это не сработает — то x станет равным 0.
Ещё в Rust есть сравнение переменной с образцом. В зависимости от того, с каким образцом совпало значение переменной, выполнится та или иная функция:
Главная особенность программ на Rust
Несмотря на синтаксис, похожий на C, главную особенность программ на Rust разработчики взяли из Haskell, и звучит она так:
Если программа на Rust скомпилировалась и не упала во время запуска, то она будет работать до тех пор, пока вы сами её не остановите.
Это значит, что программы на Rust почти так же надёжны, как программы на Haskell. Почти — потому что если программист использует «небезопасный» блок unsafe, который даёт ему прямой доступ к памяти, то в теории это иногда может привести к сбоям. Но даже с такими блоками Rust старается справляться сам и падает только в безнадёжных случаях.
Плюсы и минусы языка
Когда язык совмещает в себе несколько разных подходов из других языков, он получает большинство преимуществ каждого из них:
Минусы в основном связаны со скоростью развития языка. Так как Rust развивается очень быстро, то часто бывает так, что код из старой версии не работает в новой версии. Ещё к минусам можно добавить:
Что написано на Rust
Чаще всего Rust используют в тех проектах, где нужна стабильность и надёжность при высокой нагрузке и общее быстродействие программы.
На практике Rust подходит для разработки ОС, веб-серверов, системных программ мониторинга, веб-движков, а также для создания масштабируемых частей фронтенда и бэкенда. Например, вот самые известные проекты, где Rust был основным языком программирования:
Опровергаем четыре стереотипа о языке программирования Rust
Язык программирования Rust, начатый как хобби-проект, а впоследствии поддерживаемый корпорацией Mozilla, позволяет обычным программистам писать одновременно и безопасные, и быстрые системы: от калькуляторов до высоконагруженных серверов.
За своё относительно короткое время существования данный язык уже успел обрасти стереотипами, четыре из которых я попытаюсь опровергнуть ниже. Могу пропустить некоторые моменты, дискуссии в комментариях приветствуются.
1. Rust — сложный язык программирования
Сложность языка программирования обуславливается наличием большого числа несогласованных между собой синтаксических конструкций. Яркий пример — C++ и C#, ведь для абсолютного большинства программистов C++ является сложнейшим языком, коим не является C#, несмотря на большое количество синтаксических элементов. Rust довольно однороден, т.к. изначально был спроектирован с оглядкой на ошибки прошлого, а все нововведения вводятся исключительно при условии согласования с уже имеющимися.
Данный стереотип восходит своими корнями к концепции времён жизни ссылок (lifetimes), позволяющей на уровне семантики языка описывать гарантии действительности используемых ссылок. Синтаксис лайфтаймов выглядит сперва странным:
Но на деле синтаксис объявления лайфтайма довольно прост — это всего лишь идентификатор, перед которым следует апостроф. Лайфтайм ‘static означает, что ссылка является действительной на протяжении всего времени исполнения программы.
Что такое «действительность ссылки»? Если ссылка действительна, то она поддаётся разыменованию без паники, ошибки сегментации и прочих прелестей. Например, в функции main() указатель something становится недействительным, т.к. все автоматические переменные функции produce_something() очищаются после её вызова:
Семантически лайфтайм может быть задан посредством других лайфтаймов. Например, эта функция требует того, чтобы ссылка foo не стала недействительной до недействительности ссылки bar :
Конструкция выше применяется крайне редко, чаще всего одного лишь указания лайфтайма достаточно. Лайфтаймы — это элегантная абстракция, дающая возможность программистам быть уверенными в действительности собственных ссылок как в контексте однопоточных, так и многопоточных систем. Сложность лайфтаймов сильно переоценена.
Изучающих Rust также пугают концепции заимствования и владения (которые существовали и до создания Rust). Как и с лайфтаймами, концепции заимствования и владения лаконично вписываются в общую картину: заимствование — это взятие ссылки на значение, а владение — это связь идентификатора переменной с её значением.
Если один поток имеет иммутабельную ссылку на переменную, а другой поток имеет мутабельную, то второй может изменить значение, вызвав состояние гонки. Данная ситуация недопустима в безопасном языке, вследствие чего команда Rust определила два простых правила:
2. Rust — ещё один «убийца C/C++»
Ключевая фраза — «ещё один». На данный момент Rust — единственный язык программирования, обладающий одновременно активным сообществом и характеристиками, позволяющими ему решать задачи, решаемые языками C/C++. Синтаксис и семантика позволяют с лёгкостью изъясняться на разных уровнях абстракции — от инструкций SIMD до управления веб-серверами.
Данный стереотип возник вследствие языков Vala, Zig, Golang и подобных. Как было сказано выше, у этих языков либо слишком маленькое сообщество, либо они теоретически и практически не смогут работать на всех системах, на которых способны работать C/C++. У Vala и Zig маленькое сообщество, а Golang берёт курс на вытеснение интерпретируемых языков и не может работать на системах с критической нехваткой ресурсов, т.к. поставляется с дополнительной средой выполнения (например, сборщик мусора).
Очевидно, что языки C/C++ будут жить ещё очень много лет из-за накопленного за десятилетия кода и программистов, пишущих на них, но Rust имеет все шансы потеснить их, как это когда-то сделала Java.
3. Unsafe губит все гарантии, предоставляемые Rust
Unsafe, в действительности, — это подмножество языка Rust, разрешающее совершать лишь четыре операции, запрещённые в «безопасном» Rust. Borrow-checker не перестаёт работать, по-прежнему нельзя двум переменным обладать тем же значением и так далее. Запрещены лишь:
Потенциально небезопасный блок кода может быть инкапсулирован в безопасный блок, т.к. компилятор предполагает, что программист не сомневается в его правильности. Смысл потенциально небезопасного блока кода в том, что при его неправильном использовании поведение программы не определено.
Данный стереотип можно встретить в несколько иной трактовке: «Rust станет популярным лишь тогда, когда его сделают полностью небезопасным». И снова неверно, т.к. не все гарантии, предоставляемые Rust, могут работать в полностью небезопасном коде. Концепция Rust теряется при отсутствии гарантий безопасности.
И ещё одна трактовка: «Unsafe не работает, потому что любая программа на Rust использует небезопасные библиотеки на Си». Это заблуждение аналогично следующему: «Безопасность Java не работает, потому что любая прикладная программа исполняется на ОС, которая может содержать UB».
Дело в том, что если библиотека на Си не содержит UB, то она ни в коем случае не породит UB в использующим её коде на Rust. Существуют некоторые «оговорки», например, «UB может возникнуть как следствие неправильного использования безопасной библиотеки на Си», но если обобщить, то библиотека не является безопасной. Смотреть на определение UB из Википедии:
Другими словами, спецификация не определяет поведение языка (библиотеки, микросхемы) в любых возможных ситуациях, а говорит: «при условии А результат операции Б не определён».
4. Rust никогда не обгонит C/C++ по скорости
Утверждение безосновательное. В теории, программа, написанная на языке Rust, может быть оптимизирована столь же хорошо, как и аналогичная программа на C/C++. В некоторых синтетических тестах производительности Rust даже обгоняет GCC C:
Что касается тестов производительности на реальных задачах, то можно отметить замеры производительности RapidJSON и serde_json. serde_json парсит DOM медленнее, чем это делает RapidJSON, но при сериализации/десериализации структур serde_json обогнал RapidJSON (DOM) как на GCC, так и на CLANG:
Также можно отметить библиотеку Rustls, обогнавшую знаменитую OpenSSL практически во всех тестах (на 10% быстрее при установке соединения на сервере и на 20%-40% быстрее на клиенте, на 10%-20% быстрее при восстановлении соединения на сервере и на 30%-70% быстрее на клиенте).
По сути, когда мы сравниваем производительность Rust, скомпилированного стандартным компилятором rustc, с производительностью C/C++ кода, скомпилированного посредством CLANG, то мы сравниваем качество сгенерированного LLVM IR. В один случаях он может быть более подвержен оптимизациям, в других — менее.
На данном этапе своего развития компилятор rustc ещё не научился генерировать минимальный набор инструкций в сравнении с GCC/CLANG, что, теоретически, может замедлить выполнение программы на пару процентов. Рассмотрим два аналогичных примера кода на Rust и на C CLANG:
rustc сгенерировал больше инструкций, чем CLANG, хотя под капотом одна технология — LLVM. Это недоработка компилятора rustc, но никак не ограничение самого языка Rust, т.к. нет никаких фундаментальных препятствий сгенерировать меньший набор инструкций (как это делает CLANG).
Заключение
Rust не лишён недостатков, таких как сложности реализации некоторых структур данных, временная сырость экосистемы (которая с каждым годом всё нивелируется) и так далее. Надеюсь, что язык займёт свою нишу в современном программировании. Обсуждения — в комментариях.
Краткая история Rust: от хобби до самого популярного ЯП по данным StackOverflow
Rust — это язык системного программирования, создатели которого уделили внимание трем вещам: параллелизму, скорости и безопасности. И хотя Rust считается молодым языком программирования — его первая стабильная версия вышла в 2015 году — он разрабатывается уже более десяти лет.
Сегодня мы бы хотели заглянуть в прошлое и рассказать историю языка Rust, показать, как изменились его функции и возможности за время разработки и привести конкретные примеры внедрения этого ЯП на практике.
Личный проект (2006–2010)
Технология из прошлого, которая призвана спасти будущее от самого себя
— Грэйдон Хор (Graydon Hoare), разработчик Rust
Это одна из цитат Грэйдона Хора, которую озвучил Стив Клабник (Steve Klabnik) из команды разработчиков проекта Rust во время своей презентации на конференции ACM в 2016 году (слайды к презентации вы можете найти по ссылке, а для того, чтобы перемещаться между слайдами, используйте стрелки на клавиатуре). Эти слова хорошо отражают тот факт, что Rust — не революционный язык, имеющий передовые функции. Он просто включает в себя множество рабочих методов из «старых» языков (в том числе C++), повышая их безопасность.
Занимаясь языком, Грэйдон установил определенные правила. Он отмечал, что в первую очередь необходимо уделять внимание семантике языка, а работа над синтаксисом — это последнее дело. Поэтому в ранней реализации Rust ключевые слова были не длиннее пяти символов — язык был «кратким» и использовал такие операторы, как log, ret и fn.
Например, первый код на Rust, который увидел свет, выглядел так:
Как отмечает Стив Клабник, со временем это ограничение было снято: часть ключевых слов «удлинили», например, ret превратился в return, а часть заменили совсем. Для сравнения, в современной реализации языка вывод строки «Привет, мир!» выглядит так:
Также за время эволюции часть концепций и ключевых слов языка была убрана. Когда над языком работал Грэйдон, Rust был объектно-ориентированным и использовал оператор obj для описания объектов. Сейчас как таковое ООП языком не поддерживается, но Rust дает возможность реализовать многие его понятия с помощью абстракций.
Rust также работал с функциями параметрического полиморфизма. Концепции обобщенного программирования в языке сохранились и сейчас (оформление кода вы можете найти в этом документе), однако десять лет назад для обозначения типа параметров использовались квадратные скобки:
Самостоятельно над Rust Грэйдон работал на протяжении четырех лет. За это время ему удалось воплотить в жизнь примерно 90% задуманных функций (часть из которых имела довольно грубую реализацию). Среда для выполнения кода была завершена на 70%. Всего за это время Хор написал 38 тыс. строк кода для компилятора на OCaml.
Переход к Mozilla (2010–2012)
Я не считаю, что у языка должны быть какие-то главные особенности.
Он должен состоять из набора понятных и надежных модулей, которые хорошо «работают» в комбинации друг с другом
— Грэйдон Хор (Graydon Hoare), разработчик Rust
По прошествии четырех лет, Грэйдон решил показать свой прототип менеджеру в Mozilla. В компании проявили интерес к проекту, поскольку искали инструмент для перестройки стека браузера на более простых технологиях, чем C++. Поэтому в компании создали команду (во главе с Грэйдоном) для работы над Rust, который стал основой браузерного движка Servo.
Тогда движок Mozilla не мог полноценно работать с мультиядерными системами, поскольку имел однопоточные схемы обработки контента. Например, однопоточными были функции формирования содержимого окна и запуска JavaScript. Rust позволил разделить код рендеринга на мини-задачи, выполняемые параллельно и экономящие ресурсы центрального процессора.
Кроме ускорения работы за счет распараллеливания операций, Rust позволил повысить защищенность браузера. На тот момент Firefox был реализован на C++ и содержал 4,5 млн строк кода. C++ — это «точный» язык программирования, требующий повышенного внимания к деталям, поэтому ошибки программистов могли приводить к возникновению серьезных уязвимостей. Задачей Rust стало снижение влияния человеческого фактора с помощью компилятора.
В 2010 году разработчики языка сменили используемый до этого компилятор OCaml на компилятор, написанный на Rust. В 2011 году Грэйдон опубликовал сообщение о том, что компилятор сумел успешно «собрать» сам себя, а в 2012 команда Rust объявила о релизе альфа-версии компилятора — его документация была не полной, а скорость создания билда оказалась далека от идеальной, однако он уже поддерживал большинство функций языка и кросс-компиляцию.
Годы typesystem (2012–2014)
Наша целевая аудитория — «разочарованные разработчики C++»
В этот момент Грэйдон отошел от работы над Rust и переключился на другие проекты. Как рассказывает Стив Клабник, после этого система управления стала более «распределенной». Была сформирована федеративная структура, в которой за изменения, вносимые в разные части проекта, отвечала отдельная группа разработчиков.
Команда продолжила расти, и в ней стали появляться люди, разбирающиеся в сложных системах типов. Поэтому началось активное развитие typesystem, и все больше аспектов языка выносились в библиотеки.
Например, на ранних этапах Rust был реализован «сборщик мусора» (GC — Garbage Collector), который Грэйдон внедрил для повышения защищенности памяти. Однако потом разработчики пришли к выводу, что они могут обеспечить тот же уровень надежности с помощью системы типов, и от GC отказались.
Это решение также сказалось на системе указателей, используемой в Rust. До удаления «сборщика мусора» в языке было три основных указателя:
Период с 2012 по 2014 год — это время, когда сообщество Rust начало обретать форму. В нем образовалось три больших «лагеря»: пользователи C++, пользователи скриптовых языков и функциональные программисты. Их экспертиза повлияла на язык — постепенно он стал сочетать в себе парадигмы функционального и процедурного программирования.
В марте 2014 года также был сформирован RFC-процесс, который использовался для представления значимых изменений в языке. Решение строилось по образу и подобию Python PEP, и сейчас в нем сделано 3 тыс. коммитов. Причем в RFC попадает любое изменение, даже вносимое разработчиками. По правилам команды Rust, никто не может вносить крупные изменения, не обсудив решение с сообществом.
Релиз (2015)
Мы не знаем наверняка, что из этого получится
В начале 2015 года была выпущена версия Rust 1.0 Alpha. В ней стабилизировали ядро языка, развили систему макросов, и, наконец, закрепили за целочисленными типами int и uint названия isize и usize. В начале второго квартала того же года Rust 1.0 перешел в бету — к этому моменту репозиторий crates.io имел 1700 крэйтов (структурная единица компиляции), а количество скачиваний из репозитория превысило один миллион.
В мае 2015 года состоялся официальный релиз — Rust 1.0. Это ознаменовало начало стабильности. С этого момента все вносимые изменения должны были иметь обратную совместимость, что позволило использовать Rust в реальных проектах. Он начал находить применение в таких сферах, как game dev, веб-разработка и разработка операционных систем.
Переход в продакшн (май 2016)
Если язык хорош лишь в чем-то одном, то это — провал
В 2015 году площадка StackOverflow провела опрос среди разработчиков, в котором их попросили отметить, с какими языками программирования они работали и с какими хотели бы познакомиться. Тогда Rust занял третью строчку рейтинга. Однако годом позднее он переместился на первое место — 79% пользователей изъявили желание продолжить работу с ним.
Один из резидентов Hacker News назвал главными достоинствами языка прозрачность и простоту документации. Другие пользователи также отмечали открытость и доброжелательность Rust-сообщества, которое всегда готово помочь с изучением особенностей ЯП.
При этом многие разработчики решают продолжить работу с этим языком из-за его механизмов безопасности. Как сказал один из пользователей Reddit: «Программирование на Rust — это как паркур со страховкой и в защите; иногда это выглядит странно, но вы можете делать многие трюки, не боясь сломать себе что-нибудь».
С момента релиза стабильной версии Rust начался период его полноценного использования в продакшн. Одной из первых компаний, которые применили Rust в своем проекте, стала Mozilla. Часть «внутренностей» Firefox 45 для Linux были переписаны на Rust, а начиная с версии Firefox 47, Rust-код присутствует и в версии для Windows. Их Project Quantum, анонсированный в октябре 2016 года, также имеет в своем составе компоненты Servo.
Rust используется и в Dropbox — на этом ЯП написано ядро их продукта. Компания Dropbox создала свое новое облачное хранилище Magic Pocket, в которое перенесла информацию с Amazon S3. Изначально оно было реализовано на языке Go, но при больших нагрузках проблемой становилось высокое потребление памяти и низкая предсказуемость поведения кода на Go. Для решения этих проблем был частично задействован Rust.
В прошлом году использовать Rust для обработки пакетов JavaScript начали в npm. Rust помог исключить задержки в системе, работающей с 350 миллионами пакетов в день. Специалист службы поддержки npm Эшли Уильямс (Ashley Williams) рассказывала об опыте использования Rust на конференции RustFest в Украине. Видео вы найдете по ссылке.
Rust также используем и мы в компании Bitfury. На этом языке программирования реализован наш фреймворк для создания блокчейнов Exonum. Впервые мы представили платформу на конференции RustFest 2017, где показали её возможности и провели воркшоп, на котором продемонстрировали работу сервиса по созданию криптовалют (краткое руководство о том, как создать криптовалюту на Exonum вы можете найти здесь).
Реализация на Rust оказалась кстати при работе со смарт-контрактами. Благодаря этому умные контракты Exonum имеют большую производительность, чем контракты Ethereum (или Fabric). Код платформы полностью открыт и лежит в репозитории проекта на GitHub.
Rust также находит применение в сфере информационной безопасности. Такие проекты как Tor уже переносят часть кода на Rust.
В целом, сегодня Rust в своих продуктах используют 105 компаний. Полный их список (в котором также отмечена и Bitfury Group) можно найти на странице Friends of Rust на официальном сайте. И количество компаний, создающих программные продукты на Rust, постоянно увеличивается, чему разработчики языка очень рады.
Что делает Rust универсальным языком программирования
Долгое время Rust позиционировался исключительно как язык для системного программирования. Попытки использовать Rust для высокоуровневых прикладных задач зачастую вызывали усмешку у значительной части сообщества: зачем использовать инструмент в том качестве, на которое он не рассчитан? Какая польза от возни с типами и анализатором заимствований (borrow checker), если есть Python и Java со сборкой мусора? Но другая часть сообщества всегда видела потенциал Rust именно как языка прикладного, и даже находила его удобным в использовании для быстрого прототипирования — во многом благодаря его особенностям, а не вопреки им.
Шло время, и сейчас использование Rust для высокоуровневых прикладных задач вызывает куда меньше споров, чем раньше. Сообщество накопило практический опыт, и практика показала, что у Rust есть свои преимущества в прикладной сфере. Посмотрите, как менялось официальное определение языка, с такого:
Rust is a systems programming language that runs blazingly fast, prevents segfaults, and guarantees thread safety.
Rust — невероятно быстрый язык для системного программирования без segfault’ов и с гарантиями потокобезопасности.
A language empowering everyone to build reliable and efficient software.
Язык, позволяющий каждому создавать надёжное и эффективное программное обеспечение.
Думаю, это хорошая иллюстрация смещения акцентов в позиционировании языка.
В данной статье я хочу рассказать о важных аспектах языка Rust, которые делают его универсальным, то есть равно успешно применимым и в системном, и в прикладном программировании. Более того, попытаюсь показать, как Rust устанавливает органичную связь между этими, казалось бы независимыми и даже противоположными сферами.
Что понимать под высоким и низким уровнем?
Понятия высокого/низкого уровня, применительно к языкам программирования, уже давно перестали носить абсолютный характер. По сравнению с ассемблером, язык Си — высокоуровневый, а по сравнению с Haskell — низкоуровневый. В случае с Rust ситуация усугубляется тем, что некоторые языковые возможности в нем близки к Haskell, а некоторые — к Си. Чтобы не запутаться, что считать высоким уровнем, а что низким, я предлагаю использовать простое правило: если языковая конструкция скорее выражает что мы делаем в терминах самой задачи, то она более высокого уровня, чем та, которая скорее говорит нам как именно реализуется решение.
Интересно, что с этой точки зрения декларативный подход выше уровнем, чем императивный. Также понятно, что для системного программирования особо важно, чтобы язык мог выражать как нечто работает в системе, потому что именно это как напрямую и есть что главной задачи системного программирования. Можно сказать, что низкоуровневый по отношению к множеству разных задач язык программирования может являться одновременно высокоуровневым предметно-ориентированным языком (DSL) для сугубо системных задач, так как лучшим образом отражает архитектуру системы.
unsafe-блоки
Давайте сразу обратимся к примеру и посмотрим, как в Rust работают итераторы:
Обратите внимание, что структура Iter содержит в качестве своих полей два указателя: ptr и end (строки 2 и 3). Из-за того, что эти указатели — это обычные Си-совместимые указатели (правда NonNull дополнительно требует, чтобы указатель не был нулевым), довольно низкоуровневые ссылочные типы, их время жизни никак не отслеживается borrow checker’ом. Поэтому заданное в объявлении структуры время жизни ссылки ‘a (1) мы вынуждены добавить в «фантомное» поле с типом нулевой размерности PhantomData (4). Иначе время жизни окажется никак не используемым внутри структуры, что приведет к ошибке компиляции. То есть, другими словами: мы хотим сделать безопасный итератор, который ссылается на элементы коллекции, по которой он итерируется, и для того, чтобы он был безопасным, нам нужно учитывать время жизни ссылок. Но наша внутренняя реализация основана на указателях и потому не подразумевает никакого отслеживания времен жизни со стороны компилятора. Поэтому мы должны гарантировать своей реализацией безопасность кода, работающего с указателями (в unsafe-блоках, подобных 5), и тогда можно реализовать безопасный внешний API по всем правилам работы в safe Rust.
Это очень наглядный пример того, что представляет собой Rust на самом деле. Это высокоуровневый, безопасный язык, в котором есть низкоуровневые небезопасные возможности. Тут граница, по которой одно отделяется от другого — это определение типа, а блоки unsafe выступают маркером того, что в реализации используются весьма низкоуровневые и небезопасные средства (на самом деле в общем случае граница проходит через определение модуля: пока в язык не будет добавлена возможность помечать поля как unsafe, потенциально небезопасным становится весь код в пределах модуля, если на поведение unsafe-методов влияет содержимое полей структуры).
Важный вывод, к которому мы здесь приходим, состоит в том, что Rust — самодостаточный язык. Его высокоуровневые возможности вполне реализуются на низком уровне им же самим. И наоборот: из низкоуровневых «кирпичей» в Rust можно конструировать высокоуровневые блоки, скрывая детали реализации за безопасным API.
Теперь должно быть понятно, что unsafe, который тут и там встречается в стандартной библиотеке Rust — это не баг, а фича. Есть довольно популярный упрек к Rust со стороны: дескать, какой же это безопасный и высокоуровневый язык, если у него в std сплошные unsafe-блоки? Он либо тогда должен быть весь unsafe, либо полностью safe. Но преимущество Rust как раз состоит в том, что он позволяет делать и то и другое, при этом отделяя одно от другого. Это одна из причин, почему Rust по-настоящему универсальный язык программирования.
Макросы
Посмотрите, как организуется простейший цикл for на Python:
Они похожи, не правда ли? Но for в Rust — это просто синтаксический сахар к более низкоуровневому представлению. Вот во что разворачивается данный цикл for :
Касательно процедурных макросов: забавно, как возможность делать низкоуровневые вещи открывает языку путь к построению предельно высокоуровневых абстракций. Дело в том, что процедурные макросы в Rust — это своего рода «плагины к компилятору», которые пишутся на самом Rust. Так как Rust — это язык без сборщика мусора, то он может использоваться для создания встраиваемых компонентов. В частности, можно написать динамическую библиотеку, которую подгрузит компилятор при компиляции вашей программы, и которая будет реализовывать ваши собственные расширения языка. Взглянем на пример использования атрибутных процедурных макросов в actix-web :
Здесь #[get(..)] и #[actix_rt::main] — это пользовательские атрибуты, которые приведут при компиляции к преобразованию элементов, на которые они навешены, в соответствии с заданной программой. Вот во что развернется код выше при компиляции:
Здесь макрос позволяет указать разметку в привычном виде, декларативно, на html-подобном языке, с вкраплениями Rust-кода. Похоже на JSX, расширение языка JavaScript. Только Rust изначально обладает средствами для создания подобных расширений, для него они — обычное дело.
Возможности процедурных макросов практически безграничны. Правда, вам самим придется заботиться о семантике, так как макросы работают на синтаксическом уровне, а рефлексии в Rust не предусмотрено. Тем не менее, грамотно написанные макросы могут сильно упростить создание абстракций, специфичных для конкретной предметной области. Таким образом, сам Rust становится низкоуровневым инструментом реализации требуемого высокоуровнего предметно-ориентированного языка, предельно соответствующего решаемой задаче.
В отличие от некоторых высокоуровневых языков (таких как Python), которые служат своего рода «клеем» для низкоуровневых компонентов, написанных на других языках, Rust сам выступает и в роли «клея», и в роли инструмента реализации «склеиваемых» компонентов.
Бесплатные абстракции
Удивительно, насколько наличие абстракций с нулевой стоимостью, даже самых элементарных, упрощает прикладную разработку. Посмотрите на следующий код, который написан на языке, считающимся высокоуровневым:
И сравните с тем, как то же самое поведение реализуется в Rust:
Где вам понятнее, что происходит и где, по-вашему, вероятность ошибиться меньше? Мне кажется, что ответ очевиден.
Числовой тип — это «низкоуровневый» тип, потому что он отвечает на вопрос как значение будет представлено в памяти, а не на вопрос что оно собой представляет в контексте задачи. Но в Rust можно очень легко и элегантно вводить новые типы поверх существующих:
Несмотря на то, что оба значения a и b имеют одинаковое числовое представление, они являются объектами разных типов, и поэтому перепутать и подставить одно значение вместо другого не получится. Этот паттерн называется «Новый тип» (New type), и он совершенно бесплатен в использовании. (Подробнее о преимуществах использования паттерна «Новый тип» вы можете прочитать в замечательной статье Передача намерений.)
«Новый тип», так же как и вообще любая пользовательская структура или перечисление в Rust, может выступать границей раздела нескольких уровней программирования. И чтобы пользователь мог переходить эту границу всегда, когда это удобно для решения его задачи, эти абстракции не должны сами по себе требовать сколь-либо значимых дополнительных расходов. Иначе пользователь будет вынужден чаще пользоваться имеющимися низкоуровневыми типами, вместо того, чтобы создавать на их основе свои, высокоуровневые.
Обобщенные типы
Помимо того, что обобщенные типы избавляют от написания шаблонного кода, они являются отличным инструментом абстрагирования и высокоуровневой спецификации поведения. Вот что я имею в виду:
Выглядит как что-то низкоуровневое. Но такое поведение часто реализует определенное требование самой задачи. Например, у нас есть тип с приватным конструктором:
Можно сделать так, что UserId будет возможно сконструировать только с помощью некоего сервиса, который либо выдает новое число из глобальной последовательности идентификаторов, либо десериализует значение UserId из ранее сконструированного и сохраненного. (Подробнее о преимуществах подобного подхода к проектированию типов вы можете прочитать в статье Парсите, а не валидируйте.)
Итак, на границе высокоуровневого и низкоуровневого кода, проходящей через определения обобщенных типов, мы можем столкнуться как с высокоуровневыми, так и с низкоуровневыми ограничениями, причем далеко не всегда просто отделить одни от других. Но помочь с этим может введение новых типажей:
Теперь можно вместо набора из трех ограничений писать только одно, которое автоматически будет выполняться для всякого типа, имеющего исходные три характеристики. Таким образом можно скрыть множество низкоуровневых требований за одним высокоуровневым.
Перечисление типов
АТД во многих случаях избавляет программиста от написания низкоуровневого кода для проверки целостности и непротиворечивости типов данных. Что актуально не только для языков с динамической типизацией, но и для статически типизированных языков.
Вообще, enum в Rust используется чуть менее, чем везде — и это прекрасно! Потому что АТД — это абстракция очень высокого уровня, сравнимая с наследованием классов и полиморфизмом подтипов в ООП. Выражение традиционно низкоуровневых концепций в терминах АТД неожиданно делает их не такими уж и низкоуровневыми.
Вот как решается проблема реализации отсутствующего значения в Rust:
Подробнее об АТД и преимуществах их использования, вы можете прочитать в статье Романа Душкина «Алгебраические типы данных и их использование в программировании».
Владение
Концепция владения в Rust постулирует единственность владельца ресурса в любой момент времени. Она вводилась для решения проблемы гонки данных при конкурентном доступе и проблемы использования памяти после освобождения. Однако кроме этого, концепция владения позволила легко реализовать механизм автоматического освобождения ресурсов, где ресурсом может выступать не только память, но также файлы, сокеты и любые другие пользовательские объекты. Если владелец ресурса всегда один, то когда он выходит из области видимости и уничтожается — ресурс автоматически освобождается. Пользователь может задавать собственную процедуру освобождения, реализуя типаж Drop для своего типа.
В Java, например, с try-with-resources ответственность за корректное освобождение ресурсов перекладывается на вызывающую сторону. К тому же не всегда использование ресурсов настолько локализовано, что безошибочное использование try-with-resources очевидно. Использование Cleaner улучшает ситуацию и избавляет пользователя от необходимости следить за освобождением в тривиальных случаях, но в более сложных — головной боли не избежать (подробнее о проблемах освобождения ресурсов в Java смотрите в лекции Евгения Козлова «Вы все еще используете finalize()? Тогда мы идем к вам»).
Rust же предоставляет простой и элегантный механизм, который основан на универсальной в рамках языка концепции владения, избавляющий программиста от необходимости низкоуровневого кодирования освобождения памяти и прочих ресурсов в местах их использования.
Дополнительно, с концепцией владения тесно связан принцип перемещения по-умолчанию: если вы передаете владельца из одного места в другое, то ресурс будет перемещен (логически), а не скопирован. Это удобно использовать при реализация всевозможных переходов, например, между состояниями абстрактного автомата:
Реализация подобного сценария желательна не так уж и редко, а в некоторых случаях она крайне необходима. Так что с помощью системы владения Rust защита последовательности смены состояний становится достаточно простым делом.
Заимствование
На низком уровне заимствование означает получение ссылки на объект, время жизни которой компилятор проверит на соответствие времени жизни исходного объекта. Но при взгляде с более высокого уровня, заимствование означает получение некоего представления (view), временно соотнесенного с исходным объектом. Такое представление не обязано быть единственным.
Что же в итоге?
Как видите, Rust — это не просто очередной системный язык программирования по мотивам Си. На системный язык, обогащенный рядом высокоуровневых концепций, можно смотреть и с другой стороны: как на прикладной язык, снабженный низкоуровневым инструментарием.
Полезны ли эти низкоуровневые инструменты в прикладной раработке? Я думаю, что да. Они позволяют создавать новые эффективные высокоуровневые абстракции, расширяя арсенал разработчика. Дополнительно, наличие средств, которые позволяют изолировать и связывать между собой разные уровни, делают Rust по-настоящему универсальным языком программирования.
Полагаю, что качество Rust-кода и удобство его доработки будет напрямую зависеть от того, насколько удачно программист решил проблему инкапсуляции низких уровней относительно высоких в рамках своей задачи. Конечно, тут у Rust еще большой простор для совершенствования, но имеющихся средств уже достаточно, чтобы довольно комфортно вести прикладную, высокоуровневую разработку, а не только решать низкоуровневые системные задачи.
Upd.: Отдельное спасибо T_12 за вычитку текста статьи и дельные замечания.