В этой статье я в несколько забавном ключе документирую кое-какие размышления о своем знакомстве с Rust с позиции прожженного энтузиаста JavaScript. Здесь вас ждет импровизированная прогулка по феодам Вестероса, встреча с Ланнистерами и даже замаскированный под остров корабль — занятные аналогии, которые можно провести с работой в этом языке.
Я по достоинству оценил Rust в качестве средства для написания небольших инструментов. Мой повседневный рабочий процесс под завязку заполнен JavaScript, а так как Rust во многом его напоминает, то и знакомство с ним стало само собой разумеющимся.
Однако проделывание серьезной работы в Rust требует существенного переосмысления процесса построения кода. Компилятор здесь верен своему призванию и беспощаден. Но, скажу честно, это даже доставляет некоторое удовольствие – оттачивать код до состояния, когда он, наконец-таки, компилируется.
Хорошие новости
Современный Rust оказывается весьма схож с JavaScript. Переменные объявляются через
let
, функции выглядят очень похоже, типы уже не чужды, так как мы привыкли к TypeScript, присутствуют async/await
, да и в общем формируется весьма знакомое ощущение.Плохие новости
К сожалению, перечень хорошего завершается быстро. Суть проблемы состоит не в синтаксисе, а в способе, которым Rust рассматривает содержимое вашей программы. В высокоуровневых языках вы получаете удобные абстракции, ограждающие вас от внутренней подоплеки компьютерных процессов. И в этом есть отчетливый смысл – если вам требуется добираться на работу, то достаточно уметь водить машину – не нужно вдаваться в тонкости устройства и действия ее двигателя. В противоположность этому, низкоуровневые языки загружают вас деталями внутренних процессов, и для того, чтобы добраться до магазина за продуктами, вам уже нужно быть автомехаником.
У каждого из этих подходов есть и достоинства, и недостатки, поэтому они и используются, как правило, каждый для своей области задач. Rust же расположился ровно посередине. Он дает вам доступ ко всем внутренним процессам, попутно предоставляя прозрачность и простоту использования высокоуровневых абстракций. Но – всегда есть это «но» — вам, как разработчику, необходимо за это платить. В данном случае мы платим необходимостью научиться по-новому рассматривать построение программы.
Управлению памятью — быть!
Компьютерная программа опирается на запись значений в память. Без этого никак не обойтись, также как на кухне без ингредиентов. Следовательно, нам необходимо неким образом добыть эти ингредиенты, нарезать их в нужном количестве, закинуть в сковороду, не перепутав последовательность, и по готовности не забыть прибраться на кухне.
Высокоуровневые языки подобны заботливым родителям. Они все за вас терпеливо уберут, чтобы вы могли творить свое …искусство, не пачкая руки. Они благоразумно предоставляют вам в помощь личного Санчо Пансу – любезно именуемого «Сборщиком мусора» — поддерживающего вас в те трудные моменты, когда вы отчаянно противостоите агрессивным средам тестирования.
Когда речь заходит об управлении памятью, то Rust как бы говорит: «Ничего не знаю – реальные шеф-повара сами за собой убирают». И на то есть хорошая причина, потому что сборщик мусора несет в себе собственный набор неочевидных проблем, которые могут навредить в самый неожиданный момент. Хотя в то же время, обогащенный опытом других языков, Rust признает, что заставлять программиста управлять памятью столь же разумно, сколь поручить Дугласу Адамсу написать «Звездолет Титаник».
И для того, чтобы найти компромисс между удобством для человека и слишком сложным кодом, Rust предложил новую схему, которую можно образно обобщить как «Теперь мы все Ланнистеры».
Как розу ты ни назови, а запах ее столь же сладок
…верно сказал Шекспир. Хотя, как быть с …не столь ароматными запашками? Сохраняется ли верность его слов в обратном случае? Учитывая многовековую традицию использования людьми эвфемизмов, можно с уверенностью заключить, что нет.
Корабль длиной 55 метров, замаскированный в Тихом океане под вид маленького острова для избежания обнаружения во время Второй мировой войны. Сработало
Rust играючи демонстрирует старое-доброе искусство сбивания с толку через слова. Выдающимся перлом в его прозе можно назвать принцип «владения». Важно отметить, что в данном случае «владение» не дает ощутимых выгод владельцу, наоборот, означая, что некий родственник переложил на вас свой долг, за который теперь отдуваться вам. И сразу спешу обрадовать – дефолта по долгам здесь нет.
На землях Вестероса (смею я сказать ВестеRust?) есть крохотные, небольшие, а также крупные феоды. Загвоздка в том, что все они оккупированы Ланнистерами. Внутренне феоды занимаются своими собственными делами, а когда им требуются «товары» извне, то они берут на себя долг, чтобы эти товары получить. В последствии долг необходимо возвращать Богам Вестероса. Rust подобен королеве драконов этого мира – он узрит свысока все нюансы и проследит, чтобы все долги перед Богами были уплачены.
В нем каждая область представляет свой собственный феод. Переменные (память), которые получаются из системы и используются в этой области, остаются – как секреты в Вегасе – только внутри области. Если область свою миссию выполнила и цели больше не имеет, то память возвращается системе. Компилятор гарантирует это, поскольку тайком внедряет в создаваемые вами феоды соответствующий код – так что избежать своей участи областям не удастся. Это железное правило Rust (восхвалим уровень иронии).
Может ли возникнуть кризис?
Посмотрим, как это работает.
Здесь у нас две области видимости: внешняя
main
и внутренняя, будем звать ее inner scope
, для демонстрации. В этом случае владение работает так:main
владеетa
иb
a
хочет поработать вinner scope
, поэтомуmain
передаетa
во владениеinner scope
inner scope
делает свои дела сa
и завершается- Скрытый код Rust отбрасывает
a
main
делает свои дела сb
и тоже завершается- Rust отбрасывает
b
Обратите внимание, что долг, обусловленный владением, возвращается системе, а не области, из которой возник. Владение
a
не возвращается в область main
. Так, подождите …звучит очень опасно. Что, если у нас будет такой код?
Здесь область
main
хочет снова использовать a
, но мы сказали, что Rust уже ее отбросил по завершении inner scope
.Не даст ли программа сбой и не сгорит ли, когда достигнет этой точки выполнения?
Да, так и будет. Но, как спартанцы ответили отцу Александра, королю Филиппу II Македонскому: если она этой точки достигнет.
Абсолютный бюрократ
Компилятор Rust является гордым послушником традиции Легизма, настолько верным, что Хан Фей-цзы с восторгом бы объявил вне закона все остальные языки. Ничто не происходит в землях Rust, пока компилятор не скрепит это действие печатью «Утверждаю». Он будет тщательно проверять каждую мелочь, оценивая, насколько безопасен запуск программы, и только при удовлетворении всех требований выдаст-таки исполняемый файл.
В этом случае он понял, что наши навыки работы с долгами не соответствуют требованиям, и запрос на создание бинарника отклонил.
Программист сам должен изучить законы Rust и гарантировать их соблюдение, включая все подоплеки, хитрости и допущения. В противном случае компилятор будет сильно ругаться.
С другой стороны, команда Rust постаралась сделать все более-менее доступным, создав кучу синтаксического сахара, и, что самое важное, разборчивые сообщения об ошибках. Все это, вкупе с прекрасной документацией и великолепным сообществом, делает работу в Rust действительно увлекательной.
Rust RPG
В землях Rust переменные – это игроки. Игроки обязаны принадлежать некоторому классу – маги, священники, структуры. Более того, каждый игрок может иметь индивидуальное снаряжение. И в этом есть смысл. Ведь у вас может быть два священника — один с посохом, а второй с жезлом — не так ли?
Помните пример с
dbg!()
? Это макрос, представляющий грубый эквивалент console.log
из JS. Давайте создадим собственную типизированную переменную и выведем ее в консоль.Мы создали
struct
, которая, по сути, является типом. Затем мы создали объект этого типа. В завершении мы запросили вывод созданного объекта.Ну дела. Наш игрок до такой степени нуб, что даже не может предоставить отладочную информацию. Правда! Просто удивительно…
Ключевой момент здесь в том, что ваши рукотворные переменные все начинают в пешках без всякого снаряжения. А вот где вступает в игру и само снаряжение (на языке Rust называемое типажи (Trait)).
Нажмем F.
На этот раз работает. Единственное отличие в появившейся сверху строке. Здесь мы снаряжаем
Noob
типажом Debug
. Теперь наш игрок готов к выходу в консоль – какое достижение!В Rust представлена уйма всяческого снаряжения разной степени распространенности. При этом вполне ожидаемо, что на основе собственного дизайна с его помощью можно создавать новые вариации.
Некоторые же типажи могут генерироваться компилятором автоматически. Именно это здесь и произошло – компилятор смог избавить нас от тривиальной работы. В остальных случаях фактическая реализация ложится на ваши плечи. Хотите для мага графитовую броню? Не проблема – сделаем – но с вас код, который опишет принцип ее действия.
Типажи являются глубинным принципом работы фабрики Rust. Вернемся еще раз к примеру с владением. Если внимательно прочесть сообщение об ошибке, то мы заметим – в нем компилятор объясняет, что владение переменной пришлось «переместить», потому что
String
не реализует типаж Copy
. В противном случае компилятор не стал бы ее перемещать, а сделал копию.Типаж
Copy
означает, что вы берете раздел памяти и memcpy
его в другое место, работая непосредственно с байтами. Хорошо, значит String
не имеет типажа Copy
, нужно ли нам просто сообщить компилятору, чтобы он его присвоил? К сожалению, нет. В целях безопасного использования Copy
является слишком низкоуровневой для применения к String
.Конечно же, Rust бы не был полезен, если бы история на этом завершалась. Есть более явный типаж, который проделывает практически то же самое –
Clone
. String
обладает типажом Clone
, значит нам просто нужно использовать его вместо Copy
. В качестве примера немного подправим код:
Здесь компилятор видит, что нам нужно использовать
a
внутри inner scope
, но теперь он также видит, что мы научились все делать правильно, задействовав вместо фактической a
ее клона. Итак, получается следующее:a
принадлежитmain
- Создается
a.clone
и одалживается вinner scope
inner scope
делает свои дела и завершается- Rust отбрасывает
a.clone
main
без проблем используетa
, потому чтоa
всегда оставалась в ее владении
Красота. Естественно, это не единственный способ решить конкретно данную проблему, но он четко вписывается в наше небольшое исследование принципа владения и типажей.
Прежде чем завершать пост, считаю необходимым вкратце проговорить еще кое-что. Мы рассмотрели владение и то, как оно реализуется в области, но правда в том, что это было всего лишь упрощение. Для отслеживания владения Rust задействует принцип времени жизни (Lifetimes). Просто, получается, что чаще всего времена жизни и области совпадают. Хотя иногда компилятору все же требуется помощь. Следовательно, мы можем работать напрямую с временами жизни, а в некоторых случаях даже обязаны.
Конец?
Однозначно нет! Если сравнить Rust с айсбергом, то это даже не его вершина. Если сравнить его с тортом, то это даже не его глазировка. Если же представить Rust как криптовалюту, то это даже не сатоши. Но сегодня я явно не фонтанирую вдохновенными метафорами, так что давайте на этом закончим.
Лично мой опыт использования Rust оказался увлекателен. Кривая его изучения определенно крута, но в то же время пропорции сложности выверены удачно в сравнении с ценностью, получаемой от затраченных на это усилий. Для себя я решил, что без сомнения продолжу свое путешествие по захватывающим ландшафтам этого языка.