Несколько лет назад я отказался от всего и полностью сосредоточился на WebAssembly. В то время Rust имел наилучшую поддержку компиляции в WebAssembly, а самые полнофункциональные среды исполнения WebAssembly были основаны на Rust. Rust был лучшим из вариантов. С места в карьер я нетерпеливо начал разбираться, чем же вызван такой ажиотаж.
С тех пор мы с ещё несколькими потрясающими разработчиками создали Wick, — фреймворк приложений и среду исполнения, использующие в качестве системы основного модуля WebAssembly.
Спустя три года, выполнив несколько развёртываний в продакшен, написав электронную книгу и выпустив примерно сто пакетов на crates.io, я решил, что настало время поделиться своими мыслями о Rust.
Хорошее
Можно поддерживать больше меньшими усилиями
Я сильный сторонник разработки через тестирование (test-driven development). Я привык к тестированию в языках наподобие Java и JavaScript. Я начал писать тесты на Rust, как на любом другом языке, но обнаружил, что пишу тесты, которые не могут завершиться ошибкой. Достигнув момента, когда ваши тесты могут выполняться, то есть когда код на Rust компилируется, Rust уже учёл столько ошибок, что стандартные тестовые случаи становятся нерелевантными. Если вы избегаете блоков unsafe {}
и подверженных панике методов наподобие .unwrap()
, то начинаете с фундамента, который изначально обходит стороной множество проблем.
Агрессивность borrow checker, богатство системы типов, функциональные шаблоны и библиотеки, отсутствие «пустых» значений позволяет поддерживать больше, тратя меньше усилий на вещи наподобие тестирования. В проекте Wick я поддерживал более семидесяти тысяч строк кода с гораздо меньшим количеством тестов, чем мне бы потребовалось в других языках.
Когда необходимо писать тесты, можно легко добавлять их на лету, не задумываясь об этом. Интегрированные средства тестирования Rust позволяют практически без заминок добавлять тесты прямо рядом с кодом.
Теперь я пишу более качественный код на других языках
Программирование на Rust похоже на отношения с эмоциональным абьюзом. Rust кричит на тебя каждый день, часто ругаясь на то, что в другой жизни ты счёл бы абсолютно нормальным. Со временем ты привыкаешь к истерикам. Они становятся рутиной. Ты приучаешься ходить по струнке, чтобы избежать вспышек гнева компилятора. Как и в реальной жизни, эти изменения в поведении остаются с тобой навсегда.
Эмоциональный абьюз обычно не считается здоровым способом стимулировать к изменениям, но он всё равно меняет тебя.
Теперь я не могу писать код на других языках без ощущения дискомфорта, если строки находятся в беспорядке или когда возвращаемые значения не проверяются. Кроме того, я испытываю иррациональное раздражение, когда сталкиваюсь с ошибками при исполнении.
Clippy великолепен!
Clippy — это линтер Rust, но называть его линтером было бы медвежьей услугой. В языке, компилятор которого может довести до слёз, Clippy больше похож на вежливого друга, чем на линтер.
Стандартная библиотека Rust огромна. В ней сложно находить функции, о существовании которых вы догадываетесь, ведь так много функциональности распределено по множеству несвязанных типов, типажей, макросов и функций. Многие правила Clippy (например, manual_is_ascii_check
) ищут распространённые шаблоны, которые лучше заменить методами или типами stdlib.
У Clippy есть сотни правил, касающихся производительности, читаемости и излишних косвенных действий. По возможности он часто подсказывает код на замену.
Кроме того, похоже, что скоро мы наконец сможем конфигурировать для проекта глобальные линты. А пока нам приходится создавать хаки с решениями, чтобы поддерживать целостность линтов в проектах. В Wick мы используем скрипт для автоматического обновления встроенных конфигураций линтов для нескольких десятков крейтов. Для реализации решения этого сообществу Rust потребовались годы, что плавно подводит нас к разделу...
Плохое
Есть пробелы, с которыми придётся жить
Я сомневался в своём здравом уме каждый раз, когда возвращался к описанной выше проблеме с Clippy. Я ведь точно ошибаюсь. Наверно, есть конфигурация, которую я упустил. Я не мог в это поверить, и по-прежнему не могу. Наверняка есть способ глобального конфигурирования линтов. Я четырежды проверил это перед написанием своей статьи, чтобы убедиться, что не брежу. Эти issue теперь закрыты, но они были открыты в течение нескольких лет.
Clippy прекрасен, но этот случай — лишь один из множества в мире Rust. Я часто натыкался на библиотеки или инструменты, не соответствующие моим сценариям использования. Это довольно часто бывает с новыми языками и проектами. Чтобы повзрослеть, программному обеспечению нужно время. Но Rust не настолько новый. Чем-то он отличается от всего остального.
В опенсорсе проблемы пограничных случаев решают или рано влившиеся в проект, или новички. Именно у них возникают пограничные случаи. Их PR совершенствуют проекты, чтобы они были лучше для следующих пользователей. Большую часть десятилетия Rust награждали титулом «самого любимого языка». У него нет проблем с привлечением новых пользователей, но это не приводит к существенному совершенствованию библиотек или инструментов. Это приводит к единичным случаям форков, обрабатывающих конкретные сценарии применения. Я тоже виновен в этом, но не из-за нехватки желания отправлять PR.
Не знаю, в чём причина. Возможно, требование поддержания стабильных API, наряду с гранулярной системой типов Rust, усложняет владельцам библиотек их итеративное развитие. Сложно принять мелкое изменение, если это приведёт к смене основной версии.
А может быть, это вызвано тем, что написание кода на Rust, который делает всё для всех, чрезвычайно сложно, и разработчики просто не хотят с этим связываться.
Cargo, crates.io и структурирование проектов
Я моделировал структуру репозитория Wick на основании других найденных мной популярных проектов. Она выглядела разумно и хорошо работала, пока всё не поменялось.
При помощи Cargo можно собирать, тестировать и использовать то, что похоже на крейт размером с модуль. Но развёртывание на crates.io — это совсем другое дело.
На crates.io нельзя публиковать пакеты, если только каждый крейт, на который есть ссылка, тоже не опубликован отдельно. Это имеет некий смысл. Не хочется зависеть от крейта, зависящего от пакетов, существующих только в локальной файловой системе автора.
Однако многие разработчики естественным образом разбивают крупные проекты на более мелкие модули, но нельзя опубликовать родительский крейт, имеющий подкрейты, которые существуют только внутри себя. Нельзя даже опубликовать крейт, имеющий локальные зависимости этапа разработки. Нужно выбрать: или публиковать произвольные вспомогательные крейты, или реструктурировать проект, чтобы избежать этой проблемы. Такое ограничение кажется надуманным и необязательным. Структурированные таким образом проекты можно собирать, их нельзя только публиковать.
Дополнение: Эд Пейдж написал мне, что можно публиковаться с локальными зависимостями этапа разработки, если не включать
version
вCargo.toml
Однако Cargo имеет превосходную поддержку рабочих пространств! Рабочие пространства Cargo позволяют удобнее управлять крупными проектами, чем большинство языков. Но они не решают проблему с развёртыванием. Оказалось, что можно настроить рабочие пространства десятком разных способов, но ни один из них не упрощает развёртывание.
Эта проблема проявляет себя самим количеством вспомогательных крейтов, разработанных для упрощения публикации рабочих пространств. Каждый из них работает с подмножеством конфигураций, и «единственно верный способ» настройки рабочих пространств по-прежнему ускользает от меня. При публикации Wick часто требуется больше часа сочетания ручных повторяющихся задач с инструментами, работающими лишь частично.
Async
Асинхронность введена в Rust после его появления. Она ощущается как запоздалое решение и часто становится помехой из-за ошибок, которые трудно понять и устранить. При поиске решений нужно фильтровать информацию в зависимости от сред исполнения и стилей их async. Хотите использовать async-библиотеку? Есть вероятность, что её нельзя использовать за пределами конкретной среды исполнения async.
После двух десятков лет работы с JavaScript и приличного опыта взаимодействия с Go это наиболее существенный источник головной боли в Rust. Это преодолимая проблема, но всегда следует готовиться бороться с чудовищем async, когда оно поднимает свою голову. В других языках async практически невидим.
Ужасное
Рефакторинг может быть сложным
Богатая система типов Rust — это и благословление, и проклятие. Думать типами Rust прекрасно. Управлять типами Rust ужасно. Данные и сигнатуры функций могут иметь обобщённые типы, обобщённое время жизни и ограничения типажей. Эти ограничения могут иметь собственные обобщённые типы и время жизни. Иногда ограничений типов больше, чем самого кода.
Кроме того, дженерики нужно определять в каждом impl
. Когда пишешь код впервые, это может быть монотонным. При рефакторинге это может превратить небольшое изменение в каскадно распространяющийся хаос.
Сложно быстро двигаться вперёд, когда для одного шага необходимо изменить 14 разных определений.
Дополнение в ответ на комментарии: проблема не в выразительности, а в том, что в языке и инструментарии нет решения для снижения дублирования. Часто, бывает, нужно иметь одинаковые ограничения или ссылаться на одинаковые обобщённые списки, но нет никакого способа создания псевдонимов или как-то иначе ссылаться на центральное определение. Не уверен, должно ли оно быть, но это никак не облегчает бремя дублирования.
Вердикт
Я люблю Rust. Мне нравится то, что он может делать, и его гибкость. Я могу писать код системного уровня на том же языке, что и приложения CLI, веб-серверы и веб-клиенты. Благодаря WebAssembly я могу использовать один и тот же двоичный файл для запуска LLM в браузере, что и в командной строке. Это по-прежнему кажется мне невероятным.
Мне нравится, насколько надёжными могут быть программы на Rust. Сложно возвращаться к другим языкам, когда поймёшь, от чего тебя оберегает Rust. Я ненадолго возвращался к Go. Меня снова быстро опьянила скорость разработки. Но потом я столкнулся с паниками среды исполнения, и иллюзии разбились в прах.
Однако у Rust есть свои изъяны. Сложно искать разработчиков на нём, он медленно изучается и слишком строг для быстрых итераций. На нём сложно устранять проблемы с памятью и производительностью, особенно в асинхронном коде. Не все библиотеки хорошо справляются с безопасным кодом, а инструментарий разработки оставляет желать лучшего. Ты начинаешь с запозданием, и тебе приходится преодолевать множество препятствий. Но если ты с ними справишься, то обгонишь всех. И это «если» здесь очень важно.
Оправдал ли себя переход на Rust в нашем случае? Об этом слишком рано говорить. Мы создавали потрясающие вещи небольшой командой, но и сталкивались с серьёзными проблемами. Кроме того, у нас были технические причины для выбора Rust.
Стоит ли выбирать его вам? Если вам нужны быстрые итерации, то, вероятно, нет. Если масштаб проекта известен или вы можете пойти на первоначальные большие затраты, то определённо стоит над этим подумать. В конечном итоге вы получите практически идеальное ПО. Учитывая ежемесячно растущую популярность WebAssembly, перспектива единовременного написания идеального ПО и его повсеместного использования, скорее всего, оправдает себя.
Автор:
PatientZero