После 10 лет работы в мире Java я недавно стал более серьезно относиться к Rust. Я только начинаю программировать на Rust, но чувствую энтузиазм: я считаю, что Rust заслуживает внимания, потому что это сдвиг в балансе языков.

tl;dr

Rust может работать во встроенных микросхемах, веб-приложениях и распределенных системах. Он сочетает в себе скорость C, современную сильную систему типов, которая безопасно управляет памятью, и высококачественную экосистему. Это делает написание параллельного кода простым и безопасным. Сообщество Rust приветливое, доброе, очень активное и создает отличное программное обеспечение. Ржавчина уже используется в производстве и работает нормально, но некоторые части все еще дорабатываются. Я собираюсь присоединиться к веселью, и было бы неправильно не сообщить тебе об этом :-)

От Java до Rust

Java и Rust - очень разные языки, предположительно ориентированные на разные области. Меня спрашивали, почему Java-разработчик - скучный разработчик 😏 - заинтересовался Rust. Оказывается, мое обоснование выбора Java - то же самое, что сейчас ведет меня к Rust. Позвольте мне описать принципы, которыми я руководствовался при выборе языка и технологии: * барабанная дробь * Три закона информатики!

Три закона информатики

Существует множество правил или законов, пытающихся формализовать, что делает - или как создавать - программное обеспечение. Некоторые черпают вдохновение из книги Исаака Азимова Три закона робототехники. Законы Азимова в первую очередь забавны, потому что они представляют собой несколько причудливую научную фантастику, но они также хороши, потому что они минимальны и четко определяют приоритет.

Я назову их тремя законами информатики. Эти законы могут быть не очень оригинальными по своему содержанию - и не настоящими законами - но, как и законы Азимова, они пытаются найти наиболее существенные ортогональные аксиомы, из которых можно вывести желаемые свойства. Это законы максимальных усилий, потому что совершенство недоступно для нас.

Вот так:

  1. Программы должны быть правильными.
  2. Программы должны быть поддерживаемыми, за исключением случаев, когда это противоречит Первому закону.
  3. Программы должны быть эффективными, за исключением случаев, когда это противоречит Первому или Второму закону.

Первый закон означает, что программы должны вести себя должным образом во всех ситуациях. Это означает, что программное обеспечение должно быть лишено ошибок, проблем с безопасностью и никогда не должно давать сбоев.

Второй закон подразумевает, что программы должны быть хорошо спроектированы и документированы - потому что требуется хорошее понимание кода - и модульность , потому что большие монолиты не обслуживаются. Также должны быть инструменты, помогающие разработчикам поддерживать программы. Частичная эволюция не должна требовать полной переписывания, и другой разработчик должен иметь возможность вносить необходимые изменения из-за ясности дизайна и из-за того, что язык может быть освоен.

Третий закон означает, что программы должны стремиться к оптимальному балансу скорости и потребления ресурсов для наиболее вероятных входов, то есть в соответствии с тем, как они предназначены для использования. Это подразумевает, что мы должны использовать лучшие структуры данных для работы, стремиться к лучшим компромиссам - потому что в большинстве случаев глобального оптимума не существует -, спланируйте масштаб соответствующим образом и сделайте код сочувствующим компьютеру, на котором он запущен.

Вместе эти три закона создают требование для динамичной экосистемы и сообщества. Разработчики не могут в одиночку писать правильные, удобные в обслуживании и эффективные программы, потому что современные программы зависят от большего количества кода, чем может создать один единственный разработчик. Они должны повторно использовать компоненты, предоставленные сообществом. Они должны разделять веру в то, что сообщество является здоровым - либо растущим, либо достаточно большим, чтобы существующие компоненты были поддержаны, а новые будут построены. Это делает сообщество, окружающее язык, первостепенным фактором при выборе среды программирования.

Три закона и порядок

Правильность, несомненно, является нашей главной заботой в программном обеспечении. Если какое-то программное обеспечение не выполняет свою работу должным образом, запускать его не стоит. Если это небезопасно, это угроза безопасности. Это явно делает правильность нашим Первым законом.

Однако можно спорить, должно ли ремонтопригодность иметь большее значение, чем эффективность или наоборот. Программа потенциально может достичь лучшей производительности, если полностью игнорировать ремонтопригодность, а программирование только с учетом правильности и ремонтопригодности может привести к совершенно неэффективной программе.

На самом деле ремонтопригодность и эффективность взаимосвязаны: хорошая архитектура строится вокруг эффективного решения. Например, редактор Xi, стремящийся стать высокопроизводительным редактором следующие 20 лет, построен на структуре данных Rope. Разработка систем исключительно сверху вниз может помешать реализации эффективного решения; их разработка исключительно снизу вверх может привести к утечке слишком большого количества деталей реализации и помешать чистой, слабосвязанной архитектуре.

Тем не менее, если часть программного обеспечения правильная и чрезвычайно эффективная, но не может обслуживаться, это всего лишь черный ящик. И насколько мы уверены в его правильности, если не можем его понять? Наоборот, неэффективную, но правильную + поддерживаемую программу можно еще спасти. Ремонтопригодность становится вторым законом.

Законы и языки

Три закона информатики относятся только к программам. Тем не менее, языки программирования могут предоставлять гарантии, конструкции, инструменты, среду выполнения, сообщество или экосистему, которые помогают разработчикам создавать законопослушные программы. Посмотрим на некоторых игроков.

Системные языки

C и C ++ имеют общие свойства. Они отлично подходят для написания быстрого кода, не занимающего много места. Ручное управление памятью оставляет безопасность в руках разработчиков (которые с трудом могут вспомнить, где они оставили свой телефон!). C поставляется без батарей, поэтому вам часто приходится переписывать структуры данных; C ++ - такой монстр, что каждый проект определяет, какую часть языка они используют, а какую нет (Google известен тем, что не использует исключения). Поскольку они оба небезопасны, зависимость от другой библиотеки увеличивает риск уязвимостей безопасности.

Go заполнил пустоту, оставленную C ++, отпугивающую разработчиков. Он нацелен на то, чтобы быть быстрым и упростить написание параллельного кода с использованием подпрограмм. Это сборщик мусора, что делает его более безопасным, чем C ++. Он имеет систему простых типов, лишенную универсальных и алгебраических типов данных, которая не поддерживает повторное использование кода, как и другие современные языки. Тем не менее, у него очень активное сообщество, которое, возможно, соблазняется аурой Google и идет по стопам Docker.

Экосистема JVM

Java - достаточно простой регулярный язык. В Java существует явная история повторного использования, которая, вероятно, является лидером в плане интеграции сторонних артефактов - иногда делая большие проекты похожими на монстра Франкенштейна. Java имеет отличные инструменты, которые поддерживаются языком и виртуальной машиной. Современные JVM компилируют код «точно в срок» и превращают его в довольно эффективный собственный код. Существуют разные сборщики мусора, которые предлагают разные компромиссы. О, я уже упоминал, что сообщество Java огромно?

Kotlin пытается предоставить альтернативу «языку» Java, уменьшая многословие и обеспечивая более сильную, безопасную для null систему типов. Он в основном нацелен на JVM и имеет те же преимущества, что и Java (есть также Kotlin / Native). Инструменты, созданные JetBrains, явно превосходны. Теперь, когда он официально поддерживается для разработки под Android, он никуда не денется.

Функциональное программирование - или FP - языки

Haskell и OCaml начали свою жизнь как исследовательские проекты, но в последние годы они стали более популярными в отрасли. Они безопасны, предоставляют отличные примитивы дизайна (особенно классы и модули типов) и модель программирования, которая, по моему опыту, приводит к меньшему количеству ошибок. Оба они собирают мусор - GC был изобретен для LISP, первого языка программирования FP. В частности, Haskell является полностью чистым, что дает большие преимущества: все эффекты -, такие как IO -, явно выражены как типы, что избавляет разработчика от беспокойства о побочных эффектах. происходит неожиданно, но может стать громоздким. В их сообществах есть много исследователей, которые помогают строить прочную, формальную основу.

И еще много языков

Я не собираюсь перебирать все остальные языки. У некоторых есть очевидные сильные стороны в своей экосистеме и сообществе. Python имеет отличную экосистему для анализа данных, Erlang помогает создавать отказоустойчивые распределенные системы с использованием акторов, Scala - это более старый и дикий брат Kotlin, Clojure и Racket - современные Lisp, а TypeScript пытается понять смысл JavaScript!

Пробуждение третьего закона

Есть действительно много интересных языков. Большинство из них имеют свои сильные стороны и разделяют хорошие идеи. Насколько они помогают разработчикам, желающим следовать трем законам информатики?

Ремонтопригодность в основном обеспечивается хорошими примитивами дизайна - a.k.a языковыми конструкциями -, хорошими инструментами и сообществом. Существуют разные школы с разными мнениями о том, что представляет собой хороший набор примитивов: я лично предпочитаю те, которые выбраны современными, строго типизированными языками FP.

Если оставить в стороне ремонтопригодность, существует две группы основных языков: с ручным управлением памятью и со сборкой мусора. Поскольку мы, разработчики, несовершенны, ручное управление памятью также означает небезопасность и, следовательно, некорректность. Сборка мусора, хотя и сопряжена с накладными расходами, была де-факто стандартом в новых основных языках в течение последних 25 лет, потому что безопасность важнее, чем абсолютная производительность.

Rust - первый популярный язык, предлагающий альтернативу - автоматическое управление памятью и безопасность без сборки мусора - и он поставляется с мощными примитивами проектирования на основе FP для создания абстракций высокого уровня и многого другого.

Если теперь мы можем безопасно создавать более эффективное программное обеспечение, не так ли? Или мы должны оптимизировать производительность разработки? А можно и то и другое?

А вот и новый претендент

Rust - это язык системного программирования, который работает невероятно быстро, предотвращает сбои и гарантирует безопасность потоков. (Rust-lang.org)

Rust существует уже некоторое время, но был экспериментальным до 2015 года, когда был выпущен Rust 1.0. С тех пор язык и его экосистема выросли и значительно улучшились. Он будет улучшаться в течение многих лет, но основная команда обязалась не ломать пользовательский код.

Как и Go, Rust стремился предоставить серьезную альтернативу разработчикам, интересующимся системным программированием. Однако он заимствует (каламбур) гораздо больше из языковых исследований и успешного опыта существующих языков программирования.

Rust имеет современную систему типов, которая может автоматически управлять памятью и, наряду с проверкой границ, обеспечивать безопасный доступ к ней. В нем есть универсальные шаблоны и система признаков, близкая к классам типов Haskell. Это позволяет то, что Rust называет абстракциями с нулевой стоимостью: отличный дизайн не должен снижать производительность. Этот потрясающий доклад о встроенном программировании Джеймса Маннса описывает, как рабочая группа Rust Embedded создает набор компонентов многократного использования, чтобы абстрагироваться от различных типов аппаратных операций на крошечных микросхемах, не жертвуя затратами на выполнение.

Когда я погрузился в ресурсы Rust, начиная с замечательной Rust Book и исследуя GitHub для проектов, которые могут быть мне интересны, я заметил, насколько этот бескомпромиссный подход сформировал сообщество и экосистему. Разработчики Rust стремятся к максимальной эффективности выполнения, наиболее точным абстракциям и максимальной безопасности выполнения. Это, очевидно, делает Rust подходящим для низкоуровневого программирования, но также очень интересным для приложений более высокого уровня: хотя люди могут писать модули ядра Linux в Rust, он также используется для создания веб-приложений REST, узлов блокчейна. »И даже Одностраничные веб-приложения в WebAssembly! Давайте углубимся и посмотрим, как это помогает писать законопослушные программы.

Правильность

Трудно писать правильные программы. Часто задачи, которые мы пытаемся решить, неоднозначны. Тем не менее, языки могут помочь, будучи достаточно выразительными и не заставляя программистов прыгать через обруч, чтобы сформулировать свою проблему. Rust обладает современным набором языковых конструкций: алгебраическими типами данных (называемыми здесь перечислениями), обобщениями, характеристиками, псевдонимами типов, кортежами и т. Д. В нем также есть мощная система метапрограммирования с использованием макросов, которые могут выполнять тяжелая работа, такая как создание сериализаторов, реализации трейтов или определение встроенных DSL. Опять же, Rust заимствует отличные идеи, черпая вдохновение из Scheme и его потомка Racket, который особенно хорош при создании DSL, поддерживая гигиенические макросы.

Другая часть правильности - отсутствие неопределенного поведения, проблем с безопасностью и рисков сбоя. Rust управляет памятью без сборщика мусора, сообщая системе типов о владении. Ресурсы принадлежат одной привязке переменной, и это владение может быть передано путем передачи переменной в функцию: Rust вызывает это перемещение ресурса (в смысле, аналогичном C ++ move семантика).

Когда привязка переменной выходит за пределы своей лексической области видимости, ресурс, которым она владеет, будет отброшен - a.k.a, освобожден (можно отбросить значение раньше, просто отправив его в небытие). Хотя они доказуемо живы, ценности могут быть заимствованы либо неизменно совместно, либо взаимно исключительным образом. Это похоже на ссылки C ++, но позволяет компилятору Rust доказать, что не будет общего, изменяемого доступа к данной структуре данных, и разработчику будет о чем беспокоиться меньше.

И последнее, но не менее важное: Rust защищает разработчиков от ряда проблем параллелизма, гарантируя отсутствие совместного использования значений, не являющихся потокобезопасными, между потоками.

Есть много ресурсов для понимания владения и заимствования в Rust, но я покажу простой пример. Обратите внимание, что с правом собственности связаны и другие сложности, в частности тот факт, что некоторые типы можно копировать, а не перемещать. Например, нет смысла владеть целым числом, потому что оно настолько мало, что его можно легко скопировать. Компилятор Rust также постоянно развивается, становится умнее и допускает более интуитивный код.

В приведенном выше фрагменте создается новая строка с именем s и передается в функцию foo. Поскольку foo принимает String, а не ссылку на String, он получает право собственности, а не просто заимствует его: String был перемещен в foo . Позже, все еще находясь в main, при попытке распечатать s компилятор Rust будет жаловаться, что ресурс s, к которому был привязан, был перемещен и, следовательно, больше не доступный. Эта программа не скомпилируется.

Оказывается, foo возвращает неизмененный s, но компилятор этого не знает, как и любой разработчик, смотрящий исключительно на функцию main, Подпись foo и характеристики String. Знания этих метаданных должно быть достаточно, чтобы знать, что foo становится владельцем и наш доступ к этому ресурсу теряется.

Чтобы наша программа скомпилировалась, мы можем просто напечатать вместо нее newS. Rust даже позволяет нам снова называть его s, и это здорово, потому что s все равно нельзя было использовать после передачи его в foo! Следующая программа печатает «Bar», затем «Foo».

Если посмотреть на foo, он также создает еще одну строку, привязанную к переменной с именем s2. Он будет отброшен, когда выйдет за пределы области видимости. Пока что это очень похоже на автоматическое управление памятью для структур, выделенных в стеке в C ++ или управляемые сборщиком мусора ссылочные дескрипторы в Java. Разница в том, что ресурсы, такие как память, выделенная стеком или кучей, всегда имеют точно одного владельца.

Здесь возврат s2 вместо s в foo переместит s2 обратно к вызывающему, и программа напечатает « Бар »« Бар ». Кстати, если вам интересно, почему s2 все еще можно использовать после вызова println!, то это потому, что он println! только заимствовал его! Наконец, восклицательный знак сразу показывает, что println! - это макрос.

Наконец, нет причин выделять здесь String, потому что константы «Foo» и «Bar» уже находятся в двоичном файле. Rust может прямо указать туда и получить «кусок», который мы можем позаимствовать. Мы используем тип с именем & str, который может быть либо заимствованной строкой, либо срезом!

Ремонтопригодность

Я считаю, что система владения не только делает выполнение программы безопасным, но и делает код более удобным для сопровождения и повторного использования. Взглянув на функцию или лексическую область видимости, разработчики Rust могут определить свойства ее переменных. Они могут создавать API-интерфейсы, которые более точно передают, как их следует использовать, и вызывать помощь от средства проверки типов.

Приведу несколько примеров. В C компилятор не знает, как долго будет действителен указатель, возвращаемый функцией. На языке со сборкой мусора вы можете сохранить любую ссылку, возвращаемую функцией, и предотвратить освобождение потенциально большого фрагмента памяти. Многие библиотеки Java поставляются с документацией, в которой описывается, когда можно безопасно вызывать какие методы, как долго объекты будут находиться в допустимом состоянии, и обеспечивать соблюдение этих правил с помощью исключений или позволять вам справляться с последствиями неопределенного поведения. Это становится еще сложнее, когда задействована многопоточность. Rust позволяет возвращать ссылки, которые могут быть действительными в течение заданного времени жизни, известного во время компиляции, давать копию или предоставлять общую ссылку, которую вы можете удалить, когда захотите.

Rust поставляется с Cargo, утилитой командной строки для построения, управления зависимостями (называемой Crates), запуска тестов, исправления предупреждений и многого другого. Наличие одобренного сообществом инструмента сборки означает, что на нем можно сосредоточить усилия. Помогает то, что разработчики Cargo сделали правильный выбор, например поддержку семантического управления версиями для ящиков из коробки, с использованием удобочитаемого и редактируемого формата конфигурации (TOML) или поддержки воспроизводимых сборок. Существует также rustfmt, автоматический форматировщик, который предотвращает напрасную трату времени на ручное форматирование исходных файлов и на бесконечные споры о табуляциях и пробелах (спойлер: выиграно 4 пробела).

Тем не менее, разработка инструментов для Rust еще не завершена. У Java было преимущество в 20 лет, но сам язык очень подходил для создания инструментов. Как IDE должны поддерживать DSL в макросах? Время покажет. Существует официальный языковой сервер с интеграцией VSCode и Eclipse. Также есть плагин для IntelliJ IDEA.

Компилятор Rust поддерживается LLVM, зрелой инфраструктурой с эффективным оптимизатором. Он также может быть нацелен на WebAssembly, который позволяет писать веб-приложения на Rust и может разрешать запуск ненадежного кода в песочнице.

Мне кажется, что основные разработчики Rust активно ищут лучшие идеи, что является освежающим изменением синдрома Sun «Not Invented Here», которым так долго руководствовалась Java. Среди этих хороших идей черты Rust и отсутствие структурного наследования обеспечивают отличные примитивы проектирования, которые помогают создавать модульные и поддерживаемые системы.

Разработчики Rust сделали еще один отличный выбор - обработку ошибок. Rust имеет тип Result ‹T, E›, который может быть либо Ok (T) с успешным значением, либо Err (E) с ошибка. Программисты Haskell узнают тип Either. Обработка ошибок с помощью обычной конструкции означает, что можно использовать все обычные механизмы, включая сопоставление с образцом, передачу Result в качестве значения или его сериализацию.

Rust также использует трейты, чтобы сделать код менее подробным. Подобно Java Iterable и его циклу foreach или нотации Haskell do для монад, в Rust есть здоровая доза синтаксического сахара поверх трейтов, что делает легко создавать типы, которые кажутся естественными.

Например, свойство Rust std :: ops :: Add используется при попытке использовать операцию +. Перегрузка операторов всегда вызывала недовольство в C ++, но это также большая причина того, почему Python так силен в аналитике данных. Массивы и матрицы Numpy поддерживают те же операторы, которые мы используем на бумаге. Чтобы предотвратить конфликты, Rust позволяет только модулю, определяющему признак, или модулю, определяющему целевой тип, реализовывать признаки. Вот простой пример создания суммирования поддержки произвольного типа Point.

Эффективность

Rust быстр, работает со скоростью, сравнимой со скоростью C. Поскольку в нем нет сборщика мусора, нет скрытых затрат - даже без пауз код GC выполняется в отдельных потоках и потребляет ресурсы.

Из-за акцента на эффективности сообщество очень склонно проводить тесты для всего. Поскольку совместное использование кода - это просто и безопасно, мы можем повторно использовать эффективные структуры данных. В недавнем блоге Брайан Кантрилл сравнивает версию программы на C и Rust и приписывает 40% улучшение времени выполнения использованию BTreeSet, эффективной структуры данных, доступной прямо из коробки. Стандартные коллекции Rust.

Rust позволяет пользователям контролировать структуру памяти своих структур данных и делает косвенные ссылки явными. Это помогает писать код, дружественный к кешу, но также взаимодействует с C. FFI в Rust с C прост и не имеет накладных расходов, что упрощает вызов любого системного примитива (но он небезопасен и должен быть соответствующим образом упакован). Это то, что мы неохотно делаем в Java, особенно из соображений стабильности - segfault приведет к сбою JVM - но это может быть полезно. Например, один из самых быстрых веб-серверов Java использует JNI для вызова epoll Linux и, кажется, работает лучше, чем NIO, стандартная неблокирующая сетевая библиотека Java.

Кстати, нет смысла действовать быстро, если мы блокируем поток, ожидающий ввода-вывода. Rust поставляется с фьючерсами с нулевой стоимостью, включая неблокирующие потоки с обратным давлением. Поскольку фьючерсы и цепочки потоков могут стать подробными, можно уже использовать async / await для написания асинхронного кода, подобного идиоматическому коду Rust. Прямо сейчас await реализован как макрос, но ведется работа, чтобы сделать его стандартной функцией Rust.

Флагманская неблокирующая библиотека ввода-вывода Rust, Tokio, основана на Futures, чтобы обеспечить последовательную и плавную абстракцию для неблокирующего программирования. Tokio, в свою очередь, используется Hyper HTTP-библиотекой, которая используется веб-фреймворками.

Сообщество и экосистема

Можно увидеть огромные усилия, которые сообщество Rust вкладывает в язык и его экосистему на протяжении многих лет. Они сделали работу с Rust очень приятной и гостеприимной. Я обнаружил, что дух, оживляющий так называемых Rustaceans, приглашает и подстрекает.

У Rust есть официальные форумы и каналы обсуждения, где вы можете получить помощь и увидеть, как основные разработчики обсуждают технические вопросы. Все разрабатывается и обсуждается открыто, и предложения приветствуются.

Еженедельный информационный бюллетень дает обновления и чувство постоянного улучшения. Он выбирает Ящик недели, чтобы привлечь внимание к усилиям сообщества. Он требует помощи при возникновении проблем, иногда даже в официальном дистрибутиве Rust.

Сообщество Rust очень активно работает на GitHub и помечает многие проблемы как «Первые хорошие проблемы» для любого желающего внести свой вклад. Фактически, в Rust нет такой основы с открытым исходным кодом, как Apache или Eclipse, но все же существует сильная культура свободного программного обеспечения. Mozilla спонсирует Rust (многие основные разработчики являются сотрудниками Mozilla), но многие крупные проекты все еще живут в отдельных учетных записях GitHub.

Сообщество по-прежнему достаточно плотное, чтобы все работали вместе, чтобы построить целостную экосистему. Разработчики на Rust переписывают практически все из соображений безопасности, чтобы максимально полагаться на код Rust, а не на обернутые библиотеки C, C ++ или Go.

Разработчики могут публиковать свои крейты на crates.io. Стандартная библиотека Rust довольно мала и даже необязательна, и по замыслу большая часть разработки общих функций, таких как фьючерсы, сериализация или ведение журнала, происходит в разных ящиках. Некоторые ящики стандартизированы в процессе RFC.

Благодаря своим качествам и сообществу, Rust привлекает множество талантов. В Rust есть отличная криптографическая экосистема, библиотеки для параллелизма и параллелизма данных. Возможно, вам будет интересно использовать QUIC? Для этого есть библиотека! Вы думали о Quickcheck Haskell? "Проверять"! Или нечеткое тестирование? Интерфейсы GTK +? "Без проблем"! Вам нравится GraalVM? В Rust есть HolyJit! Nom и Pest - две библиотеки для парсинга. Люди пишут OpenGL видеоигры в Rust, другие пишут сетевые сервисы или виртуальные машины WebAssembly.

Будущее ‹Rust›

Rust создает новую сделку, предоставляя абстракции с нулевой стоимостью и смешивая набор хороших, проверенных идей с новым подходом, предлагающим бесплатную безопасность памяти. Он заново изобретает системное программирование, позволяя создавать высокоуровневые конструкции, и надежно обеспечивает высокую скорость программирования и контроль.

Тем не менее люди выбирают языки и фреймворки, потому что они продуктивны и гарантированы для конкретной цели. Если вы хотите создать веб-приложение, безопасным выбором будут Ruby on Rails или Java.

Я лично был поражен общим качеством разработки, инструментарием, производительностью и удобством Java. Здесь Rust явно менее зрелый, но я сделаю ставку на динамику действий в сообществе и на то, что все больше компаний примут Rust и помогут сделать его средой программирования мирового класса.

Через несколько лет Rust, вероятно, предоставит продуктивную и безопасную среду программирования, которая будет казаться такой же надежной и безопасной, как Java сегодня. Самое интересное - вот что получилось.