[Перевод] Вышел Rust 2018… но что это такое?
Статья написана Лин Кларк в сотрудничестве с командой разработчиков Rust («мы» в тексте). Можете прочитать также сообщение в официальном блоге Rust.
6 декабря 2018 года вышла первая версия Rust 2018. В этом релизе мы сосредоточились на производительности, чтобы разработчики Rust стали работать максимально эффективно.
Временнáя шкала показывает переход функций из бета-версии в Rust 2018 и Rust 2015. Она окружена значками для инструментов и четырёх областей: WebAssembly, embedded, networking и CLI. Красный круг — эффективность разработчика — окружает всё, кроме Rust 2015
Но вообще непросто объяснить, что такое Rust 2018.
Некоторые представляют его новой версией языка… примерно так и есть, но не совсем. Я говорю «не совсем», потому что здесь «новая версия» означает не то, что новые версии других языков.
В большинстве других языков все новые функции добавляют новую версию. Предыдущая версия не обновляется.
Система Rust действует иначе. Это связано с тем, как развивается язык. Почти все новые функции на 100% совместимы с Rust. Они не требуют каких-либо изменений. Это означает, что нет причин ограничивать их кодом Rust 2018. Новые версии компилятора продолжат поддерживать «Rust 2015 mode» по умолчанию.
Но иногда развитие языка требует инноваций, например, нового синтаксиса. И этот новый синтаксис может сломать существующие базы кода.
Например, функция async/await
. Изначально в Rust не было таких понятий. Но оказалось, что данные примитивы действительно полезны, они упрощают написание асинхронного кода.
Для этой функции необходимо добавить ключевые слова async
и await
. Но следует действовать с осторожностью, чтобы не сломать старый код, где async
или await
могли использоваться как имена переменных.
Таким образом, мы добавляем ключевые слова в Rust 2018. Хотя функция ещё не вышла, ключевые слова теперь зарезервированы. Все несовместимые изменения на ближайшие три года разработки (например, добавление новых ключевых слов), вносятся единовременно в Rust 1.31.
Хотя в Rust 2018 есть несовместимые изменения, это не значит, что ваш код сломается. Даже при наличии переменных async
и await
код будет компилироваться. По умолчанию компилятор работать как раньше.
Но если хотите использовать одну из новых функций, то можете выбрать новый режим компиляции Rust 2018. Команда cargo fix
скажет, если нужно обновить код для использования новых функций и автоматизирует процесс внесения изменений. Затем можете добавить edition=2018
к своему Cargo.toml, если согласны на использование новых функций.
Этот спецификатор версии в Cargo.toml не применяется ко всему проекту и не относится к вашим зависимостям. Он ограничен одним конкретным крейтом. То есть можно одновременно использовать крейты Rust 2015 и Rust 2018.
Поэтому даже при использовании Rust 2018 всё выглядит примерно так же, как Rust 2015. Большинство изменений внедряются одновременно в Rust 2018 и Rust 2015. Только несколько функций требуют несовместимых изменений.
Rust 2018 — это не только изменения основного языка. Далеко не только они.
Rust 2018 — это в первую очередь толчок для повышения производительности разработчиков Rust, во многом благодаря инструментам, которые находятся за пределами языка, а также благодаря отработке конкретных применений и пониманию того, как сделать Rust самым эффективным языком программирования для этих случаев.
Таким образом, вы можете представлять Rust 2018 как спецификатор в Cargo.toml, который используется для включения нескольких функций, требующих несовместимых изменений…
Или вы можете представить его ка момент времени, когда Rust становится одним из самых эффективных языков для многих применений — когда вам нужна производительность, эффективное использование ресурсов или высокая надёжность.
Мы предпочитаем второй вариант определения. Итак, давайте посмотрим на все усовершенствования, сделанные за пределами языка, а затем погрузимся в сам язык.
Язык программирования не может быть эффективным сам по себе, абстрактно. Он эффективен при конкретном применении. Поэтому мы понимали, что нужно не просто улучшить Rust как язык или инструмент. Нужно ещё и упростить использование Rust в определённых областях.
В некоторых случаях это означало создание совершенно нового набора инструментов для совершенно новой экосистемы. В других случаях — полировку уже существующих функций и хорошую документацию, чтобы стало легче поднять и запустить рабочую систему.
Команда разработчиков Rust сформировала рабочие группы по четырём направлениям:
- WebAssembly
- Встраиваемые приложения
- Сетевые задачи
- Инструменты командной строки
WebAssembly
Для WebAssembly пришлось создать совершенно новый набор инструментов.
Только в прошлом году WebAssembly сделала возможным компиляцию для запуска в интернете таких языков, как Rust. С тех пор Rust быстро стал лучшим языком для интеграции с существующими веб-приложениями.
Rust хорошо подходит для веб-разработки по двум причинам:
- Экосистема крейтов Cargo работает так, как привыкло большинство разработчиков веб-приложений. Объединяете кучу небольших модулей, чтобы сформировать более крупное приложение. Это значит, что Rust легко использовать именно там, где нужно.
- Rust потребляет мало ресурсов и не требует среды выполнения. Не нужно много кода. Если у вас крошечный модуль, выполняющий много тяжёлой вычислительной работы, внедрите несколько строчек Rust, чтобы его ускорить.
С помощью крейтов web-sys и js-sys из кода Rust легко вызвать веб-API, такие как fetch
или appendChild
. И wasm-bindgen
упрощает поддержку высокоуровневых типов данных, которые WebAssembly нативно не поддерживает.
После написания модуля Rust WebAssembly есть инструменты, чтобы легко подключить его к остальной части веб-приложения. Можете использовать wasm-pack, чтобы автоматически запустить эти инструменты, и запушить модуль в npm, если хотите.
Подробнее см. в книге «Rust и WebAssembly».
Что дальше?
После выхода Rust 2018 разработчики планируют обсуждать с сообществом, в каких направлениях работать дальше.
Встраиваемые приложения
Для встроенной разработки необходимо было повысить стабильность существующей функциональности.
Теоретически, Rust всегда был хорошим языком для встроенных приложений. Это современный инструментарий, которого катастрофически не хватало разработчикам, и очень удобные языковые функции высокого уровня. Всё это без лишней нагрузки на CPU и память. Таким образом, Rust отлично подходит для embedded.
Но на практике выходило иначе. В стабильном канале отсутствовали необходимые функции. Кроме того, для использования на встроенных устройствах требовалось изменить стандартную библиотеку. Это означает, что людям приходилось компилировать собственную версию крейта ядра Rust (крейт, который используется в каждом приложении Rust для обеспечения основных строительных блоков Rust — встроенных функций и примитивов).
В итоге разработчики зависели от экспериментальной версии Rust. И в отсутствие автоматических тестов экспериментальная сборка часто не работала на микроконтроллерах.
Чтобы исправить это, разработчики постарались перенести все необходимые функции на стабильный канал, добавить тесты к системе CI для микроконтроллеров. Это означает, что изменение какого-нибудь десктопного компонента не сломает встроенную версию.
С такими изменениями разработка встроенных систем на Rust переходит из области передовых экспериментов в область нормальной эффективности.
Подробнее см. в книге «Rust для встроенных систем».
Что дальше?
В этом году Rust обзавёлся действительно хорошей поддержкой популярного семейства ARM Cortex-M. Тем не менее, многие архитектуры ещё не так хорошо поддерживаются. Необходимо расширить Rust для аналогичной поддержки других архитектур.
Сетевые задачи
Для работы в сети необходимо было встроить в язык ключевую абстракцию: async/await
. Таким образом, разработчики могут использовать стандартные идиомы Rust даже в асинхронном коде.
В сетевых задачах часто приходится ждать. Например, ответа на запрос. Если код синхронный, то работа будет остановлена: ядро процессора, на котором выполняется код, не сможет ничего сделать, пока не придёт запрос. Но в асинхронном коде такую функцию можно поставить в режим ожидания, а ядро CPU пока займётся остальным.
Асинхронное программирование возможно и в Rust 2015, и в этом есть много преимуществ. В высокопроизводительных приложениях серверное приложение будет обрабатывать гораздо больше соединений на каждый сервер. Во встроенных приложениях на крошечных однопоточных CPU оптимизируется использование единственного потока.
Но эти плюсы сопровождаются главным недостатком: для такого кода не действует проверка заимствований и придётся использовать нестандартные (и немного путаные) идиомы Rust. Вот в чём польза async/await
. Это даёт компилятору необходимую информацию для проверки заимствований асинхронных вызовов функций.
Ключевые слова для async/await
реализованы в версии 1.31, хотя в настоящее время не поддерживаются реализацией. Бóльшая часть работы выполнена, и функция должна быть доступна в следующем релизе.
Что дальше?
Помимо эффективной низкоуровневой разработки, Rust может обеспечить более эффективную разработку сетевых приложений на более высоком уровне.
Многие серверы выполняют рутинные задачи: анализируют URL или работают с HTTP. Если превратить их в компоненты — общие абстракции, которые совместно используются как крейты — тогда будет легко подключать их друг к другу, формируя всевозможные конфигурации серверов и фреймворков.
Для разработки и тестирования компонентов создан экспериментальный фреймворк Tide.
Инструменты командной строки
Для инструментов командной строки нужно было объединить небольшие низкоуровневые библиотеки в абстракции более высокого уровня и отполировать некоторые существующие инструменты.
Для некоторых скриптов идеально подходит bash. Например, чтобы просто вызвать другие инструменты оболочки и передавать между ними данные.
Но Rust — отличный вариант для многих других инструментов. Например, если вы создаёте сложный инструмент вроде ripgrep или инструмент CLI поверх функциональности существующей библиотеки.
Rust не требует среды выполнения и компилируется в один статический бинарник, что упрощает распространение программы. И вы получаете абстракции высокого уровня, которых нет в других языках, таких как C и C++.
Что ещё может улучшить Rust? Конечно, абстракции ещё более высокого уровня.
С абстракциями более высокого уровня быстро и легко собирается готовый CLI.
Примером такой абстракции является библиотека human panic. В отсутствии такой библиотеки в случае сбоя код CLI, вероятно, выдаст всю обратную трассировку. Но она не очень интересна пользователям. Можно добавить специальную обработку ошибок, но это сложно.
С библиотекой human panic вывод автоматически направится в файл дампа ошибок. Пользователь увидит информативное сообщение, предлагающее сообщить о проблеме и загрузить файл дампа.
Начало разработки CLI-инструментов тоже стало проще. Например, библиотека confy автоматизирует его настройку. Он спрашивает только две вещи:
- Как называется приложение?
- Какие параметры конфигурации вы хотите предоставить (которые вы определяете как структуру, которая может быть сериализована и десериализована)?
Всё остальное confy определит самостоятельно.
Что дальше?
Мы абстрагировали множество задач для CLI. Но можно абстрагировать ещё кое-что. Мы собираемся выпустить больше таких библиотек высокого уровня.
Когда вы пишете на каком-то языке, вы работаете с его инструментарием: начиная с редактора и продолжая другими инструментами на всех этапах разработки и поддержки.
Это означает, что эффективный язык зависит от эффективных инструментов.
Вот некоторые новые инструменты (и улучшения существующих) в Rust 2018.
Поддержка IDE
Конечно, производительность зависит от быстрого и плавного переноса кода из сознания разработчика на экран компьютера. Здесь решающее значение имеет поддержка IDE. Для этого нужны инструменты, которые могут «объяснить» IDE смысл кода Rust: например, подсказать осмысленные варианты для автодополнения строк.
В Rust 2018 сообщество сосредоточилось на функциях, необходимых IDE. С появлением Rust Language Server и IntelliJ Rust теперь многие IDE полностью поддерживают Rust.
Более быстрая компиляция
Повышение эффективности компилятора означает его ускорение. Это мы и сделали.
Раньше, когда вы компилировали крейт Rust, компилятор заново компилировал каждый отдельный файл в крейте. Теперь реализована инкрементальная компиляция: он компилирует только те части, которые изменились. Наряду с другими оптимизациями, это сделало компилятор Rust намного быстрее.
rustfmt
Эффективность также требует, чтобы мы никогда не спорили о правилах форматирования кода и не исправляли вручную чужие стили.
В этом помогает инструмент rustfmt: он автоматически переформатирует код в соответствии со стилем по умолчанию (по которому сообщество достигло консенсуса). Rustfmt гарантирует, что весь код Rust соответствует одному стилю, подобно формату clang для C++ или Prettier для JavaScript.
Clippy
Иногда приятно иметь рядом опытного консультанта, дающего советы о лучших практиках при написании кода. Это делает Clippy: он проверяет код во время его просмотра и подсказывает стандартные идиомы.
rustfix
Но если у вас старая кодовая база с устаревшими идиомы, то самостоятельно проверять и исправлять код может быть утомительно. Вы просто хотите, чтобы кто-то внёс исправления во всю кодовую базу.
В этих случаях rustfix автоматизирует процесс. Он одновременно и применяет правила из инструментов вроде Clippy, и обновляет старый код в соответствии с идиомами Rust 2018.
Изменения в экосистеме значительно повысили эффективность программирования. Но некоторые проблемы можно решить только изменениями в самом языке.
Как мы уже говорили во вступлении, большинство языковых изменений полностью совместимы с существующим кодом Rust. Все эти изменения являются частью Rust 2018. Но поскольку они ничего не ломают, то работают в любом коде Rust… даже в старом.
Давайте посмотрим на важные функции, которые добавлены во все версии. Затем посмотрим на небольшой список особенностей Rust 2018.
Новые функции для всех версий
Вот небольшой пример новых возможностей, которые есть (или будут) во всех версиях языка.
Более точная проверка заимствований
Одно большое преимущество Rust — это проверка заимствований. Она гарантирует, что код безопасен для памяти. Но это также довольно сложная функция для новичков в Rust.
Частично сложность заключается в изучении новых понятий. Но есть и другая часть… Проверка заимствований иногда отклоняет код, который вроде бы должен работать с точки зрения программиста, который вполне понимает концепцию безопасности для памяти.
Нельзя заимствовать переменную, потому что она уже заимствована
Такое происходит, потому что время жизни заимствования предположительно должно было распространяться до конца своей области — например, до конца функции, в которой находится переменная.
Это означало, что даже если переменная завершила работу со значением и больше не пытается получить доступ, другим переменным всё равно отказывают в доступе к этому значению до конца функции.
Чтобы исправить ситуацию, мы сделали проверку умнее. Теперь она видит, когда переменная фактически завершила использовать значение. После этого она не блокирует использование данных.
Пока это доступно только в Rust 2018, но в ближайшее время функцию добавят во все остальные версии. Скоро мы напишем подробнее на эту тему.
Процедурные макросы в стабильном Rust
Макросы в Rust были ещё до Rust 1.0. Но в Rust 2018 сделаны серьёзные улучшения, например, появились процедурные макросы. Они позволяют добавить в Rust собственный синтаксис.
Rust 2018 предлагает два вида процедурных макросов:
Макросы, подобные функциям
Макросы, подобные функциям, позволяют создавать объекты, которые выглядят как обычные вызовы функций, но фактически выполняются во время компиляции. Они берут один код и выдают другой, который компилятор затем вставляет в двоичный файл.
Они существовали раньше, но с ограниченем. Макрос мог выполнять только оператор match. У него не было доступа, чтобы посмотреть все токены во входящем коде.
Но с процедурными макросами вы получаете те же входные данные, что и парсер: тот же поток токенов. Это означает, что можно создавать гораздо более мощные макросы, подобные функциям.
Макросы, подобные атрибутам
Если вы знакомы с декораторами на языках вроде JavaScript, макросы атрибутов очень похожи. Они позволяют аннотировать фрагменты кода на Rust, которые следует предварительно обработать и превратить в нечто иное.
Макрос derive
делает именно это. Когда вы помещаете его над структурой, компилятор принимает эту структуру (после того, как она проанализирована как список токенов) и обрабатывает её. В частности, добавляет базовую реализацию функций из трейта.
Более эргономичные заимствования в сопоставлениях
Тут незамысловатое изменение.
Раньше, если вы хотели что-то заимствовать и пытались выполнить сопоставление, нужно было добавить какой-то странный синтаксис:
Теперь вместо &Some(ref s)
пишем просто Some(s)
.
Самая малая часть Rust 2018 — это функции, специфичные именно для этой версии. Вот небольшой набор изменений в Rust 2018.
Ключевые слова
В Rust 2018 добавлено несколько ключевых слов:
try
async/await
Эти функции ещё не полностью реализованы, но ключевые слова добавлены в Rust 1.31. Таким образом, в будущем не придётся вводить новые ключевые слова (что стало бы несовместимым изменением), когда мы реализуем эти функции.
Модульная система
Одна большая боль для новичков Rust — модульная система. И понятно почему. Трудно было понять, почему Rust выбирает тот или иной модуль. Чтобы исправить это, мы внесли некоторые изменения в механизм путей.
Например, если вы импортировали крейт, то можете использовать его в пути на верхнем уровне. Но если переместить любой код в подмодуль, он больше не будет работать.
// top level module
extern crate serde;
// this works fine at the top level
impl serde::Serialize for MyType { ... }
mod foo {
// but it does *not* work in a sub-module
impl serde::Serialize for OtherType { ... }
}
Другой пример — префикс ::
, который используется и для корня крейта, и для внешнего крейта. Трудно понять, что перед нами.
Мы сделали это более явным. Теперь если вы хотите обратиться к корневому крейту, то используете префикс crate::
. Это лишь одно из улучшений для понятности.
Если вы хотите, чтобы текущий код использовал возможности Rust 2018, скорее всего, потребуется обновить код с учётом новых путей. Но необязательно делать это вручную. Перед добавлением спецификатора версии в Cargo.toml просто запустите cargo fix
— и rustfix
внесёт необходимые изменения.
Всю информацию о новой версии языка содержит «Руководство по Rust 2018».