18 факторов, способствующих революции Rust, часть 1 из 3

Фотография на обложке Marie P на Unsplash

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

Где мы были

В 2019 году, во время работы в Keyrock, команда разработчиков (около 4-6 человек в тот период) достигла предела возможностей нашего прототипа торговых сервисов на JavaScript, когда дело дошло до обработки сложного мира высокочастотной торговли (HFT). Мы должны были обрабатывать несколько потоков информации одновременно и иметь данные, доступные в течение определенного времени, внутри одного сервиса, с надежностью. Конечно, я понимаю, что JS, вероятно, был не лучшим выбором, но у нас были возможности для всей команды, а вы когда-нибудь пробовали создавать прототипы на JS? Это так быстро… В любом случае, мы быстро создали прототип нашего первого поколения сервисов на JavaScript, и хотя JS подходит для многих случаев использования, (первое сильное мнение в статье) HFT — не один из них. Не то чтобы это останавливало нас от попыток 😇.

При выборе новой технологии для второго поколения сервисов мы прошли через фазу исследования, пробуя Go-lang, Kotlin, Elixir, а также еще один язык, с которым я возился… Rust. Мы знали, что не хотим идти по пути C++ по причинам, о которых я расскажу в другой статье. Я провел несколько тестов Rust с базовыми алгоритмами сортировки и не мог поверить в их скорость и продолжал рассказывать об этом в нашем офисе в Брюсселе. Другой старший разработчик оспорил эту технологию (по сравнению с тем, что мы использовали в производстве) и сказал, что не хочет продолжать слушать мои рассказы о ней, если она не будет значительно быстрее, чем то, что мы имели; он сам провел несколько более глубоких экспериментов и вскоре стал нашим самым ярым эмиссаром Rust. Rust обладал необходимой нам скоростью, доступными примитивами параллелизма, приемлемым синтаксисом и отличным инструментарием. Мы с головой окунулись в этот новый мир, и сегодня большая часть нашего производственного кода написана на Rust.

Однако в этом языке, даже на ранних стадиях, было что-то еще, что я никак не мог понять. Хотя иногда с ним было сложно работать, в то время я знал, что наше решение было правильным, хотя и не мог объяснить, почему, помимо скорости, отсутствия сборщика мусора и поддержки параллелизма. Было ли это рискованно? Я так не думал — мы провели (всего лишь) достаточно исследований, и лично у меня в голове звучала философия Apple — что мы должны использовать возможность как устранить технологию, которую мы исчерпали, так и заменить ее новой технологией, находящейся на заре своего развития.

Три года спустя я могу четко выразить то, что, как мне кажется, я чувствовал интуитивно — и я чувствую, что вынужден написать об этом! Если вы новичок в Rust, я знаю, куда вы направляетесь… это мир первоначального замешательства и разочарования, за которым в конечном итоге следует чувство, что это просто имеет смысл.

Сегодня мы видим Rust повсюду. Блокчейн, веб-сервисы, библиотеки, поддерживающие модули Python и JavaScript, даже внутри святая святых — ядра Linux. Эта статья из трех частей посвящена тому, почему это так.


Почему это происходит?

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

Из опроса разработчиков Stack Overflow 2021 года

Мы можем начать с тех факторов, которые легко понять и принять, а затем я перейду к менее очевидным, но чрезвычайно мощным возможностям языка.

Давайте начнем со всего списка, а затем разобьем его на части:

Часть 1

  • Синтаксис
  • Разделение данных и кода
  • Безопасность типов
  • Декларативное программирование и функции функциональной композиции

Часть 2

  • Развитое управление пакетами
  • Готовность к параллелизму std lib и отличная поддержка async
  • Понятные, полезные ошибки компилятора
  • Абстракции с нулевой стоимостью (Оптимизация)
  • Энергоэффективность
  • WASM
  • Tauri — высокопроизводительные, безопасные, кроссплатформенные настольные вычисления

Часть 3

  • Макросы!
  • Криптовалюта и блокчейн
  • Все остальные инструменты
  • Скорость
  • Отсутствие сборки мусора
  • Функции безопасности памяти

Синтаксис

В университете у меня был некоторый опыт работы с Pascal и Smalltalk (это было давно), до этого я кодировал на ассемблере Motorola 68000 series на Commodore Amiga, а еще раньше (мое первое знакомство с низкоуровневым кодированием) я играл с машинным кодом на MOS Technology 6510 в Commodore 64.

Почему я делюсь этим? Потому что синтаксис кодирования в том мире не был стилем «C». В последующие годы я также использовал C, C++, Java, JavaScript, Perl, C#, а позже R, которые известны как языки типа C или C-семейства. (Python немного отличается, но это недалеко от него). Другие языки, перечисленные выше, таковыми не являются, и это часто приводит к разочарованию в изучении языка и трудностям в поиске людей, которые могут «конвертировать» в эти языки.

Приведенные выше языки C-типа — это, на самом деле, очень маленькая выборка из числа языков, использующих синтаксис C-семейства (около 73). Почему их так много? После успеха C & C++ было выдвинуто предположение, что при изучении нового языка будет гораздо проще погрузиться в знакомый синтаксис. Например, компания Sun Microsystems работала над проектом по очистке и улучшению C++ за счет возможности запускать его везде — он назывался Oak. Позже им понадобилось изменить название, и они назвали его Java.

Тот факт, что Rust сохраняет эту «ДНК» C-типа, является преднамеренным. Системное программирование — это в основном сфера деятельности разработчиков C & C++, с небольшой группой разработчиков Objective-C (супермножество C) и Swift во вселенной Apple. Rust чтит это наследие и нацелен на этих разработчиков, объединяя очень знакомый синтаксис с символами и функциями, которые облегчают работу с новыми парадигмами таким образом, что в начале пути Rust лишь слегка отвлекает, а когда вы привыкаете к нему, в нем появляется много смысла.

У меня были заявки на техзадание от разработчиков C++ с опытом работы с Rust всего 1-2 месяца, и они «поняли» его, часто гораздо быстрее, чем разработчики с другим опытом. Эти работы вовсе не являются идиоматическим Rust, но они показывают силу знакомого синтаксиса.

Разделение кода и данных

Я большой поклонник функционального программирования (ФП) — и в частности Elixir и Erlang. Первоначальный подзаголовок здесь был таким: Разумный шаг от эпохи непонятной объектно-ориентированной философии и архитектуры в сторону разделения кода и данных с использованием трейтов и функций».

Это длинный заголовок раздела. Я переписывал его несколько раз. Было действительно трудно найти название, которое отражало бы этот фактор, и не вызвало бы массового недоумения у массы приверженцев запутанной и обычно неправильно понимаемой интерпретации объектно-ориентированного программирования (ОО, или ООП), которые потратили годы на обучение и разработку методов и шаблонов для структурирования кода, основанных на подходе, который имеет практически нулевые доказательства, а вместо этого стал результатом миллионов, потраченных на маркетинг, в течение 3-4 десятилетий, плюс направленные усилия нетехнических менеджеров по разделению кодирования на «единицы работы».

Существуют различные истории, которые документируют это (моя любимая — работа вечно приветливого Ричарда Фельдмана, автора языка программирования Elm). Очень краткое резюме:

Давным-давно программы состояли из данных и процедур и часто представляли собой сильно связанный беспорядок обмена данными. Затем парень по имени Алан (Kay, 1966) сказал, что программы должны передавать сообщения вместо обмена данными (идея из Simula, 1962). Он и другие создали язык объектов с ассоциированными функциями, которые передавали сообщения между объектами (Smalltalk ’72). Но, похоже (если говорить коротко), он был несколько неправильно понят, и более поздние языки реализовали эти идеи со своими собственными странными предположениями.


«C++ является значительным препятствием для изучения и правильного применения объектно-ориентированных методологий и технологий».

— Брюс Вебстер, Подводные камни объектно-ориентированной разработки
(Нью-Йорк: M&T Books, 1995), 139 (цитируется по Hsu, Mahoney — см. ниже).


Алан также попытался объяснить некоторую путаницу много лет спустя:

«Я придумал термин «объектно-ориентированный», и могу сказать, что не имел в виду C++».

— Алан Кей, OOPSLA ’97


«ООП для меня означает только обмен сообщениями, локальное сохранение, защиту и сокрытие состояния-процесса, а также экстремальную позднюю привязку всех вещей».

— Алан Кей, 1998, https://wiki.c2.com/?AlanKayOnMessaging


«Мне жаль, что я давно придумал термин «объекты» для этой темы, потому что он заставляет многих людей сосредоточиться на менее значимой идее».

— Алан Кей, 2014, http://lists.squeakfoundation.org/pipermail/squeak-dev/1998-October/017019.html


К сожалению, к тому времени мир в основном отодвинул в сторону функциональное программирование и знаковые теории информатики, такие как теория акторной модели Карла Хьюитта (которые были частично вдохновлены Smalltalk ’72), и развил идеи «ОО-суперсолдата» Objective-C и C++, превратив их в неконтролируемого «Халка» Java. (язык, который когда-то был настолько популярен, что в честь него был назван другой, в основном не связанный с ним язык, и мутировал, чтобы выглядеть как он).


«Объектно-ориентированное программирование, например, рассматривалось одним из приверженцев как способ навязать традиционные отношения на рабочем месте в производстве. Тейлоризм подразумевался во всех предлагаемых решениях кризиса».

— Хсу, Корнельский университет, 2020 год


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

Есть исключения: Управление обработчиком побочного эффекта (для меня) практически всегда должно быть архитектурой, похожей на «объект + методы», потому что это просто замечательно работает (например, код драйвера DB, обернутый в Class или Struct + Methods в Rust), потому что в этом случае существует эксклюзивная ассоциация между кодом и типом данных. Но для гибкой, разумной, удобной в обслуживании архитектуры параллельных, распределенных систем (имеется в виду массовая взаимосвязь функциональности) есть менее хлопотные, более гибкие, элегантные и тестируемые альтернативы (для примера, пожалуйста, почитайте Карла Хьюитта и спорьте с ним и другими, например, создателями Erlang, а не со мной, пожалуйста. Или лучше сначала посмотрите превосходные видео Брайана Уилла и Ричарда Фельдмана, ссылки на которые приведены ниже).


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

Если у вас ссылочно прозрачный код, если у вас чистые функции — все данные приходят на входные аргументы и все выходят, не оставляя после себя никакого состояния — это невероятно многоразовое использование».

— Джо Армстронг о возможности повторного использования программного обеспечения.


К счастью, боль от «заученного» способа ОО-кодирования наконец-то привела мир к открытию преимуществ FP (даже у Java есть гибридный преемник FP/OO — Kotlin) и альтернативных практик и архитектурных подходов. Mozilla и основная команда Rust, к счастью, выбрали альтернативный архитектурный подход при создании Rust: Traits. На мой взгляд, это был гениальный ход, поскольку он привел к тому, что (что очень забавно) люди переходят на архитектурные практики FP, даже не зная, что они это делают, часто жалуясь на FP.

В качестве простого введения, разработчики Rust могут создавать структуры данных под названием Structs и связанные с ними функции и/или методы (с явной самоссылкой). Однако это имеет ограничения при создании абстракций и в конечном итоге приводит к открытию трейтов Rust. Трейты похожи на интерфейсы в некоторых ОО-языках. Обычно трейты состоят из сигнатур функций, которые должны быть выражены структурой, претендующей на выражение этого трейта. Таким образом, они похожи на набор взаимосвязанных моделей поведения для некоторого неизвестного типа. Не совсем чистая функция, но ограниченная функция, с ожиданиями относительно типа, который будет преобразован функцией.

И что? Ну… это значит, что мы можем:

  1. Создать набор чистых функций, которые обрабатывают данные, или мы можем…
  2. определить набор методов как поведение для некоторого общего типа с некоторыми ожидаемыми характеристиками, или…
  3. мы можем создать известную структуру с набором признаков и выраженных функций для этих признаков, специально разработанных для этой структуры — немного похоже на объект + методы — но с признаком мы сохраняем принцип того, что структура — или «объект» — открыта для расширения — один из первых пяти принципов объектно-ориентированного дизайна, все из которых все еще могут быть выражены в Rust без постоянного соединения данных с кодом.

Такое отсутствие мнений в возможностях языка означает, что он обладает достаточной гибкостью (например) для создания приложений с превозносимой архитектурой «функциональное ядро, императивная (OO) оболочка», и при этом предоставляет возможность опираться на безопасность Rust (см. Часть 3) при кодировании таким образом. Таким образом, Rust становится невероятным языком, который позволяет использовать широкий спектр архитектурных стилей. Он говорит: «Не бойтесь! Если вам нравится боль от ранних архитектурных предположений, которые ОО навязывает через миф об инкапсуляции, вы все еще можете почувствовать эту боль с Rust!» 😉

Подробнее:

Sun готовит 500-миллионное продвижение бренда Java

Объектно-ориентированное программирование — это плохо, Брайан Уилл, Youtube, 2016 г.

Почему функциональное программирование не является нормой? Фельдман, Youtube, 2019

Связь между кризисом программного обеспечения и объектно-ориентированным программированием, Хансен Хсу

Серебряной пули нет, Брукс, 1987

Функциональное ядро, императивная оболочка, destroyallsoftware.com, 2012 by @garybernhardt

Безопасность типов

Статические типы

В некоторых из немногих проведенных исследований были отмечены преимущества статически типизированных языков. Если вы не слышали о Quorum, ознакомьтесь с ними — они привносят доказательства в разработку программного обеспечения — отрасль, где наши методы были предписаны менеджерами и сформированы из желания отменить и контролировать разработчиков (см. также Hsu, выше), а не путем сбора доказательств лучшей практики.

Для удобства разработчиков в IDE статические типизированные языки выигрывают круглые сутки, как и в случае с автоматической проверкой ошибок (например, функциональные сигнатуры) в коде. Типизация настолько важна для некоторых разработчиков, что для JavaScript был создан целый надмножественный язык, обеспечивающий соблюдение типов (аплодисменты Microsoft — 👍 вы сделали доброе дело. Мне не часто удается это сказать).

Динамические типы

Послушайте, я не продаю этот пункт — я люблю JavaScript, когда играю или помогаю друзьям/родственникам изучать код. Но если я хочу создать финансовую систему/торгового бота и т.д., в этом контексте явность рулит. Я не хочу нигде угадывать вывод типов. Но мне очень нравятся динамические типы; вводим Rust Generics и Traits…

Rust Generics

Система generics в Rust позволяет указывать трейты (см. раздел выше), которые выражает «тип». В упрощенном виде это означает, что мы можем использовать динамические типы (общий тип), но ограничивать эти типы теми, которые выражают определенный набор трейтов. Часто кажется, что это лучшее из двух миров. Это может создать некоторую сложность в некоторых областях, но в целом это очень хорошо работает для моделирования сложных абстракций.

Дополнительная информация:

Доказательства человеческого фактора при разработке языка, Quorum

Программное обеспечение как трудовой процесс, Ensmenger & Aspray, 2002

TypeDevil: Динамический анализ несогласованности типов для JavaScript, Pradel, Shuh & Sen

The Plot to Deskill Software Engineering, Glass, Communications of the ACM, 2015

Декларативное программирование и особенности функциональной композиции

Существуют общие проблемы, связанные с попыткой постоянно инкапсулировать мир в виде объектов и моделей поведения (см. раздел «Разделение кода и данных», выше). Как только эти поведения начинают смешиваться, вариации требуют все более и более сложных объектов с несколькими родителями и специфическими вариациями методов, что часто приводит к дублированию кода и коду, который трудно поддерживать при необходимости внесения изменений (например, гибридные объекты, которые облегчают/абстрагируют связь между совершенно несвязанными объектами) — То, как метод выражает поведение (знания о нем), распространяется на «детей» объектов (это не очень DRY-код и не очень гибкий). Существует целая индустрия правил и паттернов, помогающих справиться с этим беспорядком.

Функциональное программирование использует парадигму, которая помогает решать подобные сценарии довольно элегантным способом: Функциональная композиция. Общие, «чистые» функции заменяют методы, привязанные к данным, и вместо этого «обрабатывают» данные, соответствующие определенным характеристикам. Таким образом, каждая унаследованная вариация структуры данных может быть обработана набором функций, которые описывают поведение, а затем эти поведения могут быть составлены для создания более сложных поведений. Но каждое поведение выражается только один раз (#極度乾燥). Вы, вероятно, уже использовали эти подходы, если играли с итераторами или потоками.

Как Rust облегчает эту задачу? С помощью генериков, трейтов, Fn() и связанных с ними типов, закрытий и итераторов. Самое важное, что эти возможности не навязываются вам с принудительной внутренней неизменяемостью, как в других языках FP. Вместо этого Rust предоставляет шведский стол функциональных возможностей, которые можно использовать, а можно и не использовать — но это очень плотный шведский стол, что приводит к двум результатам: Опытные разработчики FP, как правило, не голодают, а новички в FP могут учиться, экспериментировать и постепенно вводить архитектурные изменения.

Небольшой пример, который будет понятен разработчикам FP, но может раздражать или сбивать с толку новичков в парадигме FP: По умолчанию все переменные неизменяемы, а функции неявно возвращают свое «конечное значение», или результат их конечного выражения, до тех пор, пока выражение не заканчивается точкой с запятой. Если значения нет (если есть точка с запятой, завершающая последнее выражение), функция все равно вернет пустой кортеж. В совокупности это означает, что функции Rust (по умолчанию, без использования ключевого слова mut) не имеют внутренних изменяемых переменных. Они обрабатывают некоторые входные данные и выдают новое значение/ы на основе этих данных. Если затем убрать использование mut для входных параметров функции, мы получим функцию обработки данных в стиле FP, которая не может изменять свои входные или внутренние переменные, и таким образом получается идемпотентная функция (всегда производит тот же эффект, что и при первом запуске).

Как это можно использовать с традиционными циклами и ветвлениями if/else? Ну, это было бы неуклюже — поэтому Rust также содержит высокоуровневые абстракции, такие как map, filter, fold и другие редукторы, которые позволяют создавать итераторы, применяющие функции; привет декларативное программирование! Правильно структурированный, он дает возможность создавать высоко оптимизированный безветвистый код — но безветвистый код, который все еще читабелен!

Если это звучит чуждо для вашего «нормального» мира кодирования, скорее всего, вы исходите из императивной/объектно-ориентированной парадигмы программирования — и это нормально. Даже не зная определения этих загадочных слов: Мы можем добавить mut к параметрам функции, и mut перед переменными, и все обычные инструменты if/else/for/while присутствуют.

Таким образом, Rust является уникальным инструментом для перехода от одной парадигмы к другой.

Дополнительная информация:

https://www.freecodecamp.org/news/imperative-vs-declarative-programming-difference/

https://www.fpcomplete.com/blog/2018/10/is-rust-functional/

https://github.com/hemanth/functional-programming-jargon

Branchless Equivalents of Simple Functions


Это все для первой части. Во второй части я собираюсь сосредоточиться на:

  • Развитое управление пакетами
  • Поддержка параллелизма в std-lib и отличная поддержка async
  • Понятные, полезные ошибки компилятора
  • Абстракции с нулевой стоимостью (оптимизация)
  • Энергоэффективность
  • WASM
  • Tauri — высокопроизводительные, безопасные, кроссплатформенные настольные вычисления

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *