Что программируют на rust
Первые шаги по Rust
Всем привет. Недавно познакомился с новым для себя языком программирования Rust. Я заметил, что он отличается от других, с которыми мне до этого доводилось сталкиваться. Поэтому решил покопать глубже. Результатами и своими впечатлениями хочу поделиться:
Сразу поясню, что я около десяти лет пишу на Java, так что рассуждать буду со своей колокольни.
Killer feature
Rust пытается занять промежуточное положение между низкоуровневыми языками типа C/C++ и высокоуровневыми Java/C#/Python/Ruby… Чем ближе язык находится к железу, тем больше контроля, легче предвидеть как код будет выполняться. Но и имея полный доступ к памяти намного проще отстрелить себе ногу. В противовес С/С++ появились Python/Java и все остальные. В них нет необходимости задумываться об очистки памяти. Самая страшная беда — это NPE, утечки не такое уж частое явление. Но чтобы это все работало необходим, как минимум, garbage collector, который в свою очередь начинает жить своей жизнью, параллельно с пользовательским кодом, уменьшая его предсказуемость. Виртуальная машина еще дает платформонезависимость, но на сколько это необходимо — спорный вопрос, не буду его сейчас поднимать.
Rust является низкоуровневым языком, на выходе компилятор выдает бинарник, для работы которого не нужны дополнительные ухищрения. Вся логика по удалению ненужных объектов интегрируется в код в момент компиляции, т.е. сборщика мусора во время выполнения тоже нет. В Rust так же нет пустых ссылок и типы являются безопасными, что делает его даже более надежным чем Java.
В основе управления памятью лежит идея владения ссылкой на объект и одалживания. Если каждым объектом владеет только одна переменная, то как только кончается срок ее жизни в конце блока, все на что она указывала можно рекурсивно очистить. Также ссылки можно одалживать для чтения или записи. Тут работает принцип один писатель и много читателей.
Эту концепцию можно продемонстрировать в следующем куске кода. Из метода main() вызывается test(), в котором создается рекурсивная структура данных MyStruct, реализующая интерфейс деструктора. Drop позволяет задать логику для выполнения, перед тем как объект будет уничтожен. Чем-то похоже на финализатор в Java, только в отличие от Java, момент вызова метода drop() вполне определен.
Вывод будет следующим:
Т.е. перед выходом из test() память была рекурсивно очищена. Позаботился об этом компилятор, вставив нужный код. Что такое Box и Option опишу чуть позже.
Таким образом Rust берет безопасность от высокоуровневых языков и предсказуемость от низкоуровневых языков программирования.
Что еще интересного
Далее перечислю черты языка по убыванию важности, на мой взгляд.
Тут Rust вообще впереди планеты всей. Если большинство языков пришли к тому, что надо отказаться от множественного наследования, то в Rust наследования нет вообще. Т.е. класс может только имплементировать интерфейсы в любом количестве, но не может наследоваться от других классов. В терминах Java это означало бы делать все классы final. Вообще синтаксическое разнообразие для поддержания OOP не так велико. Возможно, это и к лучшему.
Для объединения данных есть структуры, которые могут содержать имплементацию. Интерфейсы называются trait и тоже могут содержать имплементацию по умолчанию. До абстрактных классов они не дотягивают, т.к. не могут содержать полей, многие жалуются на это ограничение. Синтаксис выглядит следующим образом, думаю комментарии тут не нужны:
Из особенностей на которые я обратил внимание, стоит отметить следующее:
Еще немного безопасности
Как я уже говорил Rust уделяет большое внимание надежности кода и пытается предотвратить большинство ошибок на этапе компиляции. Для этого была исключена возможность делать ссылки пустыми. Это мне чем-то напомнило nullable типы из Kotlin. Для создания пустых ссылок используется Option. Так же как и в Kotlin, при попытке обратиться к такой переменной, компилятор будет бить по рукам, заставляя вставлять проверки. Попытка же вытащить значение без проверки может привести к ошибке. Но этого уж точно нельзя сделать случайно как, например, в Java.
Мне еще понравилось то, что все переменные и поля классов по умолчанию являются неизменяемыми. Опять привет Kotlin. Если значение может меняться, это явно надо указывать ключевым словом mut. Я думаю, стремление к неизменяемости сильно улучшает читабельность и предсказуемость кода. Хотя Option почему-то является изменяемым, этого я не понял, вот код из документации:
Перечисления
В Rust называются enum. Только помимо ограниченного числа значений они еще могут содержать произвольные данные и методы. Таким образом это что-то среднее между перечислениями и классами в Java. Стандартный enum Option в моем первом примере как раз принадлежит к такому типу:
Для обработки таких значений есть специальная конструкция:
А также
Я не ставлю себе целью написать учебник по Rust, а просто хочу подчеркнуть его особенности. В этом разделе опишу, что еще есть полезного, но, на мой взгляд, не такого уникального:
Ложки дегтя
Этот раздел необходим для полноты картины.
Killer problem
Главный недостаток происходит из главной особенности. За все приходится платить. В Rust очень неудобно работать c изменяемыми графовыми структурами данных, т.к. на любой объект должно быть не более одной ссылки. Для обхода этого ограничения есть букет встроенных классов:
И это неполный список. Для первой пробы Rust, я опрометчиво решил написать односвязный список с базовыми методами. В конечном счете ссылка на узел получилась следующая Option >:
Выглядит так себе, итого три обертки вокруг одно объекта. Код для простого добавления элемента в конец списка получился очень громоздкий, и в нем есть неочевидные вещи, такие как клонирования и одалживания:
На Kotlin то же самое выглядит намного проще:
Как выяснил позже подобные структуры не являются характерными для Rust, а мой код совсем неидиоматичен. Люди даже пишут целые статьи:
Тут Rust жертвует читабельностью ради безопасности. Кроме того такие упражнения еще могут привести к зацикленным ссылкам, которые зависнут в памяти, т.к. никакой garbage collector их не уберет. Рабочий код на Rust я не писал, поэтому мне сложно сказать насколько такие трудности усложняют жизнь. Было бы интересно получить комментарии практикующих инженеров.
Сложность изучения
Долгий процесс изучения Rust во многом следует из предыдущего раздела. Перед тем как написать вообще хоть что-то придется потратить время на освоение ключевой концепции владения памятью, т.к. она пронизывает каждую строчку. К примеру, простейший список у меня занял пару вечеров, в то время как на Kotlin то же самое пишется за 10 минут, при том что это не мой рабочий язык. Помимо этого многие привычные подходы к написанию алгоритмов или структур данных в Rust будут выглядеть по другому или вообще не сработают. Т.е. при переходе на него понадобится более глубокая перестройка мышления, просто освоить синтаксис будет недостаточно. Это далеко не JavaScript, который все проглотит и все стерпит. Думаю, Rust никогда не станет тем языком, на котором учат детей в школе программирования. Даже у С/С++ в этом смысле больше шансов.
В итоге
Мне показалась очень интересной идея управления памятью на этапе компиляции. В С/С++ у меня опыта нет, поэтому не буду сравнивать со smart pointer. Синтаксис в целом приятный и нет ничего лишнего. Я покритиковал Rust за сложность реализации графовых структур данных, но, подозреваю, что это особенность всех языков программирования без GC. Может быть, сравнения с Kotlin было и не совсем честным.
В этой статье я совсем не коснулся многопоточности, думаю это отдельная большая тема. Еще есть планы написать какую-нибудь структуру данных или алгоритм посложнее списка, если есть идеи, прошу поделиться в комментариях. Интересно было бы узнать приложения каких типов вообще пишут на Rust.
Почитать
Если вас заинтересовал Rust, то вот несколько ссылок:
UPD: Всем спасибо за комментарии. Узнал много полезного для себя. Исправил неточности и опечатки, добавил ссылок. Думаю, такие обсуждения сильно способствуют изучению новых технологий.
Rust — молодой и дерзкий язык программирования
Говорят, что это одновременно C++ и Haskell.
Первая версия языка Rust появилась в 2010 году, и он сразу занял третью строчку в списке любимых языков разработчиков на StackOverflow. Год спустя Rust возглавил этот список и держался там несколько лет. Давайте посмотрим, почему этот язык стал таким популярным, в чём его особенности и почему вокруг него много споров.
В чём идея языка Rust
Автору языка нравилась скорость работы и всемогущество языка C++ и надёжность Haskell. Он поставил перед собой задачу совместить оба этих подхода в одном языке, и за несколько лет он собрал первую версию языка Rust.
Rust позиционируется как компилируемый системный мультипарадигмальный язык высокого уровня. Сейчас поясним, что это значит.
👉 Компилируемый язык означает, что готовая программа — это отдельный файл, который можно запустить на любом компьютере с нужной операционной системой. Для запуска не нужно устанавливать среду разработки и компилятор, достаточно, чтобы скомпилированная версия подходила к вашему компьютеру.
👉 Системный — это когда на языке пишут программы для работы системы в целом. Это могут быть операционные системы, драйверы и служебные утилиты. Обычные программы тоже можно писать на Rust — от калькулятора до системы управления базами данных. Системный язык позволяет писать очень быстрые программы, которые используют все возможности железа.
👉 Мультипарадигмальный значит, что в языке сочетаются несколько парадигм программирования. В случае Rust это ООП, процедурное и функциональное программирование. Причём, ООП в Rust пришло из C++, а функциональное — из Haskell. Программист может сам выбирать, в каком стиле он будет писать код, или совмещать разные подходы в разных элементах программы.
Синтаксис и код
За основу синтаксиса в Rust взят синтаксис из C и C++.Например, классический «Привет, мир!» на Rust выглядит так:
fn main() <
println!(«Hello, world!»);
>
Если вы знакомы с подобным синтаксисом, то сможете быстро начать писать и на Rust. Другое дело, что в Rust есть свои особенности:
let x = if new_game() < 4 >
else if reload() < 3 >
else
Последнее разберём подробно. При такой записи переменная x будет равна четырём, если функция new_game() вернёт значение true. Если этого не случится, компилятор вызовет функцию reload() и проверит, что получилось. Если true, то x примет значение 3, а если и это не сработает — то x станет равным 0.
Ещё в Rust есть сравнение переменной с образцом. В зависимости от того, с каким образцом совпало значение переменной, выполнится та или иная функция:
Главная особенность программ на Rust
Несмотря на синтаксис, похожий на C, главную особенность программ на Rust разработчики взяли из Haskell, и звучит она так:
Если программа на Rust скомпилировалась и не упала во время запуска, то она будет работать до тех пор, пока вы сами её не остановите.
Это значит, что программы на Rust почти так же надёжны, как программы на Haskell. Почти — потому что если программист использует «небезопасный» блок unsafe, который даёт ему прямой доступ к памяти, то в теории это иногда может привести к сбоям. Но даже с такими блоками Rust старается справляться сам и падает только в безнадёжных случаях.
Плюсы и минусы языка
Когда язык совмещает в себе несколько разных подходов из других языков, он получает большинство преимуществ каждого из них:
Минусы в основном связаны со скоростью развития языка. Так как Rust развивается очень быстро, то часто бывает так, что код из старой версии не работает в новой версии. Ещё к минусам можно добавить:
Что написано на Rust
Чаще всего Rust используют в тех проектах, где нужна стабильность и надёжность при высокой нагрузке и общее быстродействие программы.
На практике Rust подходит для разработки ОС, веб-серверов, системных программ мониторинга, веб-движков, а также для создания масштабируемых частей фронтенда и бэкенда. Например, вот самые известные проекты, где Rust был основным языком программирования:
Введение в программирование на Rust
Apr 17 · 12 min read
Rust — это перспективный язык программирования, набирающий рекордную популярность для низкоуровневых систем, таких как операционные системы и компиляторы.
В 2020 году по итогам опроса разработчиков Stack Overflow самым любимым языком программирования уже пятый год подряд был признан Rust. Многие разработчики уверены в том, что Rust скоро обгонит C и C++ благодаря своему средству проверки заимствований и решению давних проблем, таких как управление памятью, а также неявная и явная типизация.
Сегодня мы поможем вам начать р а боту с Rust независимо от вашего уровня опыта. Мы расскажем, что отличает Rust от других языков, изучим его основные компоненты и поможем написать вашу первую программу на Rust!
Вот что мы рассмотрим в статье.
Что такое Rust?
Rust — это мультипарадигмальный статически типизированный язык программирования с открытым исходным кодом, используемый для создания операционных систем, компиляторов и других программно-аппаратных средств. Он был разработан Грейдоном Хором в Mozilla Research в 2010 году.
Rust оптимален с точки зрения производительности и безопасности, причем акцент здесь сделан на безопасном параллелизме. Этот язык больше всего похож на C или C++, но использует средство проверки заимствований для подтверждения безопасности ссылок.
Rust — это идеальный язык системного программирования для разработки встроенного программного обеспечения для платформ без операционной системы. Наиболее распространено применение Rust в низкоуровневых системах, например ядрах операционных систем или в микроконтроллерах.
Rust отличается от других низкоуровневых языков отличной поддержкой параллельного программирования с предотвращением гонки данных.
Зачем изучать Rust?
Язык программирования Rust идеально подходит для низкоуровневого системного программирования из-за системы выделения памяти с уникальной концепцией владения и приверженности оптимальному и безопасному параллелизму. И хотя его все еще нечасто используют в крупных компаниях, Rust остается одним из языков, получающих самые высокие оценки.
Rust продолжает совершенствоваться в условиях непрекращающегося роста требований к низкоуровневым системам и вполне способен стать языком завтрашних операционных систем. Попробуйте себя в роли разработчика Rust уже сейчас, чтобы получить желаемую должность, надолго обеспечив себя работой с высокой оплатой.
«Hello World!» на Rust
Разберем все части этого кода.
fn — это сокращение от function («Функция»). В Rust (как и в большинстве других языков программирования) функция как бы говорит: «Сообщите мне информацию, а я сделаю то-то и то-то и затем дам ответ».
Функция main — это то место, где начинается программа.
Скобки содержат список параметров для этой функции. Сейчас он пуст, то есть параметров нет. Но скоро мы увидим много функций с параметрами.
Дальше идет строка. Строки состоят из нескольких собранных вместе букв или символов. Для обозначения строки эти символы помещаются в кавычки ( » ). Затем строки передаются для макросов типа println! и других функций, с которыми мы еще поиграем.
А это точка с запятой. Она обозначает конец одной инструкции, как точка в предложении. Инструкции — это указания компьютеру выполнить конкретное действие. Чаще всего инструкция состоит из всего одной строки кода. В нашем случае она вызывает макрос. Есть и другие виды инструкций, которые мы скоро увидим.
Основы синтаксиса Rust
Теперь рассмотрим основные части программы на Rust и способы их реализации.
Переменные и их изменяемость
Переменные — это точки данных, которые сохраняются и помечаются для последующего использования. Формат объявлений переменных таков:
Имя переменной должно быть информативным, т. е. описывать, чем является ее значение. Например:
Совет💡 Всегда давайте переменным названия, начинающиеся со строчной буквы, а новое слово начинайте с заглавной.
В Rust переменные неизменяемы по умолчанию, т. е. их значение нельзя изменить после того, как они заданы.
Например, вот этот код выдаст ошибку во время компиляции:
На первый взгляд такое свойство языка Rust кажется неудобным, но оно помогает применять лучшие практики минимизации изменяемых данных. Ведь наличие изменяемых данных часто приводит к появлению багов, если как минимум две функции ссылаются на одну и ту же переменную.
Чем больше у вас становится переменных и функций, тем легче случайно изменить их значения. Такого рода ошибки поддаются отладке с трудом, поэтому в Rust предпочитают избегать их в принципе.
Чтобы переопределить это значение по умолчанию и создать изменяемую переменную, объявим ее вот так:
Типы данных
Пока что мы видели, что значения переменных задаются либо с помощью фраз (называемых строками), либо целых чисел. Эти переменные представляют собой различные типы данных, которые обозначают, какой вид имеют содержащиеся в них значения и какие операции они выполняют.
В этом случае наш пример с объявлением my_name будет переписан следующим образом:
Явная типизация позволяет соответствующим образом определять тип переменной и избегать ошибок в тех случаях, когда тип переменной неочевиден. Rust сделает наиболее правильное с его точки зрения предположение, но это может привести к неожиданному поведению.
Во избежание недопонимания со стороны других разработчиков и для недопущения синтаксической ошибки поменяем объявление следующим образом:
Основные типы на Rust:
Функции
Функции — это наборы связанного кода на Rust, объединенные под кратким условным обозначением и вызываемые из других частей программы.
Вот формат для объявления функции:
Это уже знакомое нам сокращение от function («Функция»). За ним в коде Rust следует объявление функции.
Здесь находится идентификатор функции, который будет использоваться при ее вызове.
Эти скобки заполняются любыми параметрами, которые нужны функции. В данном случае никаких параметров не передается, поэтому скобки оставлены пустыми.
А здесь передаваемому значению присваивается имя. Это имя выступает в роли имени переменной, ссылающейся на параметр в любом месте тела функции.
После параметра необходимо явно указать тип. Во избежание путаницы неявная типизация параметров в Rust запрещена.
Фигурные скобки обозначают начало и конец блока кода. Код внутри скобок выполняется при каждом вызове идентификатора функции.
А это заполнитель для кода функции. Лучше не включать сюда никакого кода, не связанного прямо с выполнением задачи функции.
Добавим немного кода. Переделаем hello-world в функцию say_hello() :
Совет💡 Увидели () — значит, вы имеете дело с вызовом функции. Если параметров нет, получаем внутри скобок пустое поле параметров. Сами скобки все равно остаются, указывая на то, что это функция.
Вот как будет выглядеть полная программа:
Комментарии
Комментарии — это сообщения, которые содержат описание того, для чего нужен тот или иной сегмент кода. Они помогают автору кода быстро вспомнить логику дальнейших своих действий, а другим программистам — быстро понять, как устроена программа. Так что написание хороших комментариев полезно всем.
Совет💡 используйте комментарии для «закомментирования» разделов кода, выполнение которых не требуется, но которые позже понадобится добавить.
Условные инструкции
Условные инструкции — это способ создания поведения, которое имеет место только в случае истинности некоего набора условий. С помощью этих инструкций получаются адаптируемые функции, которые отлично справляются с различными программными ситуациями без использования второй функции.
Допустим, нужно сделать функцию для создания учетной записи для любого пользователя, у которого еще нет учетной записи для авторизации в системе.
Вот как выглядит формат оператора if :
Совет💡 необходимо, чтобы в циклах while проверяемая переменная была изменяемой. Если переменная никогда не меняется, такой цикл будет продолжаться бесконечно.
Промежуточный Rust: владение и структуры
Владение
Владение — это центральная особенность Rust и одна из причин такой его популярности.
Во всех языках программирования должна предусматриваться система освобождения неиспользуемой памяти. В некоторых языках, таких как Java, JavaScript или Python, есть сборщики мусора, которые автоматически удаляют неиспользуемые ссылки. В низкоуровневых языках типа C или C++ от разработчиков требуется всякий раз, когда это необходимо, выделять и освобождать память вручную.
Ручное выделение памяти сопряжено с многочисленными проблемами, поэтому практиковать его затруднительно. Когда память выделяется на очень продолжительное время, она расходуется впустую. А слишком раннее освобождение памяти, как и выделение одной и той же памяти дважды, приводит к ошибке.
Rust выгодно отличает от всех этих языков система владения, которая управляет памятью с помощью набора правил, применяемых компилятором во время компиляции.
Вот эти правила владения.
А теперь посмотрим, как владение уживается с функциями. Для объявленных переменных память выделяется, пока они используются. Если эти переменные передаются в качестве параметров в другую функцию, выделение перемещается или копируется к другому владельцу и используется у него.
Структуры
Аналогом этих структур в таких языках, как Java и Python, являются классы.
Вот синтаксис объявления структуры:
Каждый создаваемый экземпляр типа Car должен иметь значения для этих полей. Поэтому создадим экземпляр Car для конкретного автомобиля со значениями для brand (модели) и year (года выпуска).
Точно так же, как при определении переменных с примитивными типами, определяем переменную Car с идентификатором, на который будем ссылаться позже.
Вот как выглядит вся структура целиком:
В целом структуры отлично подходят для хранения вместе всей информации, относящейся к тому или иному типу объекта, для реализации и обращения к ней в программе.
Система сборки Rust: Cargo
Cargo — это система сборки и диспетчер пакетов Rust. Это важный инструмент для организации проектов на Rust. Здесь приводится перечень библиотек, необходимых проекту (они называются зависимостями). Он автоматически загружает любые отсутствующие зависимости и собирает программы на Rust из исходного кода.
Программы, с которыми мы имели дело до сих пор, достаточно просты, и поэтому зависимости для них не нужны. А вот при создании более сложных программ вам понадобится Cargo с возможностями инструментов, недоступных в рамках стандартной библиотеки. Cargo также используется для загрузки проектов в портфолио на GitHub, так как они хранят все части и зависимости вместе.
Если скачать Rust с официального сайта, Cargo автоматически устанавливается вместе с компилятором ( rustc ) и генератором документации ( rustdoc ) как часть набора инструментальных средств Rust. Убедиться, что Cargo установлен, помогает ввод в командной строке следующей команды:
Для создания проекта с Cargo запустите в интерфейсе командной строки операционной системы следующее:
Чтобы все это увидеть, наберите:
Продвинутые концепции для дальнейшего изучения
Несмотря на то, что многие из этих компонентов кажутся маленькими, с каждым из них можно шаг приблизиться к полному освоению Rust! Год от года Rust становится все более популярным, а это значит, что сейчас самое время обзавестись навыками для создания низкоуровневых систем будущего.
Обзор языка программирования Rust
Rust — новый экспериментальный язык программирования, разрабатываемый Mozilla. Язык компилируемый и мультипарадигмальный, позиционируется как альтернатива С/С++, что уже само по себе интересно, так как даже претендентов на конкуренцию не так уж и много. Можно вспомнить D Вальтера Брайта или Go от Google.
В Rust поддерживаются функицональное, параллельное, процедурное и объектно-ориентированное программирование, т.е. почти весь спектр реально используемых в прикладном программировании парадигм.
Я не ставлю целью перевести документацию (к тому же она весьма скудная и постоянно изменяется, т.к. официального релиза языка еще не было), вместо этого хочется осветить наиболее интересные фичи языка. Информация собрана как из официальной документации, так и из крайне немногочисленных упоминаний языка на просторах Интернета.
Первое впечатление
Синтаксис языка строится в традиционном си-подобном стиле (что не может не радовать, так как это уже стандарт де-факто). Естественно, всем известные ошибки дизайна С/С++ учтены.
Традиционный Hello World выглядит так:
Пример чуть посложнее — функция расчета факториала:
Как видно из примера, функции объявляются в «функциональном» стиле (такой стиль имеет некоторые преимущества перед традиционным «int fac(int n)»). Видим автоматический вывод типов (ключевое слово let), отсутствие круглых скобок у аргумента while (аналогично Go). Еще сразу бросается в глаза компактность ключевых слов. Создатели Rust дейтсвительно целенаправленно сделали все ключевые слова как можно более короткими, и, скажу честно, мне это нравится.
Мелкие, но интересные синтаксические особенности
Типы данных
Rust, подобно Go, поддерживает структурную типизацию (хотя, по утверждению авторов, языки развивались независимо, так что это влияние их общих предшественников — Alef, Limbo и т.д.). Что такое структурная типизация? Например, у вас в каком-то файле объявлена структура (или, в терминологии Rust, «запись»)
type point =
Вы можете объявить кучу переменных и функций с типами аргументов «point». Затем, где-нибудь в другом месте, вы можете объявить какую-нибудь другую структуру, например
type MySuperPoint =
и переменные этого типа будут полностью совместимы с переменными типа point.
В противоположность этому, номинативная типизация, принятая в С, С++,C# и Java таких конструкций не допускает. При номинативной типизации каждая структура — это уникальный тип, по умолчанию несовместимый с другими типами.
Структуры в Rust называются «записи» (record). Также имеются кортежи — это те же записи, но с безымянными полями. Элементы кортежа, в отличие от элементов записи, не могут быть изменяемыми.
Имеются вектора — в чем-то подобные обычным массивам, а в чем-то — типу std::vector из stl. При инициализации списком используются квадратные скобки, а не фигурные как в С/С++
Вектор, тем ни менее — динамическая структура данных, в частности, вектора поддерживают конкатенацию.
Есть шаблоны. Их синтаксис вполне логичен, без нагромождений «template» из С++. Поддерживаются шаблоны функций и типов данных.
Язык поддерживает так называемые теги. Это не что иное, как union из Си, с дополнительным полем — кодом используемого варианта (то есть нечто общее между объединением и перечислением). Или, с точки зрения теории — алгебраический тип данных.
В простейшем случае тег идентичен перечислению:
В более сложных случаях каждый элемент «перечисления» — самостоятельная структура, имеющая свой «конструктор».
Еще интересный пример — рекурсивная структура, с помощью которой задается объект типа «список»:
Теги могут участвовать в выражениях сопоставления с образцом, которые могут быть достаточно сложными.
Сопоставление с образцом (pattern matching)
Для начала можно рассматривать паттерн матчинг как улучшенный switch. Используется ключевое слово alt, после которого следует анализируемое выражение, а затем в теле оператора — паттерны и действия в случае совпадения с паттернами.
В качестве «паттеронов» можно использовать не только константы (как в Си), но и более сложные выражения — переменные, кортежи, диапазоны, типы, символы-заполнители (placeholders, ‘_’). Можно прописывать дополнительные условия с помощью оператора when, следующего сразу за паттерном. Существует специальный вариант оператора для матчинга типов. Такое возможно, поскольку в языке присутствует универсальный вариантный тип any, объекты которого могут содержать значения любого типа.
Указатели. Кроме обычных «сишных» указателей, в Rust поддерживаются специальные «умные» указатели со встроенным подсчетом ссылок — разделяемые (Shared boxes) и уникальные (Unique boxes). Они в чем-то подобны shared_ptr и unique_ptr из С++. Они имеют свой синтаксис: @ для разделяемых и
для уникальных. Для уникальных указателей вместо копирования существует специальная операция — перемещение:
после такого перемещения указатель x деинициализируется.
Замыкания, частичное применение, итераторы
С этого места начинается функциональное программирование. В Rust полностью поддерживается концепция функций высшего порядка — то есть функций, которые могут принимать в качестве своих аргументов и возвращать другие функции.
1. Ключевое слово lambda используется для объявления вложенной функции или функционального типа данных.
В этом примере мы имеем функцию make_plus_function, принимающую один аргумент «x» типа int и возвращающую функцию типа «int->int» (здесь lambda — ключевое слово). В теле функции описывается эта самая фунция. Немного сбивает с толку отсутствие оператора «return», впрочем, для ФП это обычное дело.
2. Ключевое слово block используется для объявления функционального типа — аргумента функции, в качестве которого можно подставить нечто, похожее на блок обычного кода.
Здесь мы имеем функцию, на вход которой подается блок — по сути лямбда-функция типа «int->int», и вектор типа int (о синтаксисе векторов далее). Сам «блок» в вызывающем коде записыавется с помощью несколько необычного синтаксиса <|x| x + 1 >. Лично мне больше нравятся лямбды в C#, символ | упорно воспринимается как битовое ИЛИ (которое, кстати, в Rust также есть, как и все старые добные сишные операции).
3. Частичное применение — это создание функции на основе другой функции с большим количеством аргументов путем указания значений некоторых аргументов этой другой функции. Для этого используется ключевое слово bind и символ-заполнитель «_»:
Чтобы было понятнее, скажу сразу, что такое можно сделать на обычном Си путем создания простейшей обертки, как-то так:
const char* daynum (int i) < const char *s =<"mo", "tu", "we", "do", "fr", "sa", "su">; return s[i]; >
Но частичное применение — это функциональный стиль, а не процедурный (кстати, из приведенного примера неясно, как сделать частичное применение, чтобы получить функцию без аргументов)
Еще пример: объявляется функция add с двумя аргументами int, возвращающая int. Далее объявляется функциональный тип single_param_fn, имеющий один аргумент int и возвращающий int. С помощью bind объявляются два функциональных объекта add4 и add5, построенные на основе функции add, у которой частично заданы аргументы.
Функциональные объекты можно вызывать также, как и обычные функции.
4. Чистые функции и предикаты
Чистые (pure) функции — это функции, не имеющие побочных эффектов (в том числе не вызывающие никаких других функций, кроме чистых). Такие функции выдяляются ключевым словом pure.
Предикаты — это чистые (pure) функции, возвращающие тип bool. Такие функции могут использоваться в системе typestate (см. дальше), то есть вызываться на этапе компиляции для различных статических проверок.
Синтаксические макросы
Планируемая фича, но очень полезная. В Rust она пока на стадии начальной разработки.
Выражение, аналогичное сишному printf, но выполняющееся во время компиляции (соответственно, все ошибки аргументов выявляются на стадии компиляции). К сожалению, материалов по синтаксическим макросам крайне мало, да и сами они находятся в стадии разработки, но есть надежда что получится что-то типа макросов Nemerle.
Кстати, в отличие от того же Nemerle, решение выделить макросы синтаксически с помощью символа # считаю очень грамотным: макрос — это сущность, очень сильно отличающаяся от функции, и я считаю важным с первого взгляда видеть, где в коде вызываются функции, а где — макросы.
Атрибуты
Концепция, похожая на атрибуты C# (и даже со схожим синтаксисом). За это разработчикам отдельное спасибо. Как и следовало ожидать, атрибуты добавляют метаинформацию к той сущности, которую они аннотируют,
Придуман еще один вариант синтаксиса атрибутов — та же строка, но с точкой с запятой в конце, аннотирует текущий контекст. То есть то, что соответствует ближайшим фигурным скобкам, охватывающим такой атрибут.
Параллельные вычисления
Пожалуй, одна из наиблее интересных частей языка. При этом в tutorial на данный момент не описана вообще:)
Программа на Rust состоит из «дерева задач». Каждая задача имеет функцию входа, собственный стек, средства взаимодействия с другими задачами — каналы для исходящей информации и порты для входящей, и владеет некоторой частью объектов в динамической куче.
Множество задач Rust могут существовать в рамках одного процесса операционной системы. Задачи Rust «легковесные»: каждая задача потребляет меньше памяти чем процесс ОС, и переключение между ними осуществляется быстрее чем переключение между процессами ОС (тут, вероятно, имеются в виду все-же «потоки»).
Задача состоит как минимум из одной функции без аргументов. Запуск задачи осуществляется с помощью функции spawn. Каждая задача может иметь каналы, с помощью которых она передает инфорацию другим задачам. Канал — это специальный шаблонный тип chan, параметризируемый типом данных канала. Например, chan — канал для передачи беззнаковых байтов.
Для передачи в канал используется функция send, первым аргументом которой является канал, а вторым — значение для передачи. Фактически эта функция помещает значение во внутренний буфер канала.
Для приема данных используются порты. Порт — это шаблонный тип port, параметризируемый типом данных порта: port — порт для приема беззнаковых байтов.
Для чтения из портов используется функция recv, аргументом которой является порт, а возвращаемым значением — данные из порта. Чтение блокирует задачу, т.е. если порт пуст, задача переходит в состояние ожидания до тех пор, пока другая задача не отправит на связанный с портом канал данные.
Связывание каналов с портами происходит очень просто — путем инициализации канала портом с помощью ключевого слова chan:
let reqport = port();
let reqchan = chan(reqport);
Несколько каналов могут быть подключены к одному порту, но не наоборот — один канал не может быть подключен одновременно к нескольким портам.
Typestate
Общепринятого перевода на русский понятия «typestate» я так и не нашел, поэтому буду называть это «состояния типов». Суть этой фичи в том, что кроме обычного контроля типов, принятого в статической типизации, возможны дополнительные контекстные проверки на этапе компиляции.
В том или ином виде состояния типов знакомы всем программистам — по сообщениям компилятора «переменная используется без инициализации». Компилятор определяет места, где переменная, в которую ни разу не было записи, используется для чтения, и выдает предупреждение. В более общем виде эта идея выглядит так: у каждого объекта есть набор состояний, которые он может принимать. В каждом состоянии для этого объекта определены допустимые и недопустимые операции. И компилятор может выполнять проверки — допустима ли конкретная операция над объектом в том или ином месте программы. Важно, что эти проверки выполняются на этапе компиляции.
Например, если у нас есть объект типа «файл», то у него может быть состояние «закрыт» и «открыт». И операция чтения из файла недопустима, если файл закрыт. В современных языках обычно функция чтения или бросает исключение, или возвращает код ошибки. Система состояний типов могла бы выявить такую ошибку на этапе компиляции — подобно тому, как компилятор определяет, что операция чтения переменной происходит до любой возможной операции записи, он мог бы определить, что метод «Read», допустимый в состоянии «файл открыт», вызывается до метода «Open», переводящего объект в это состояние.
В Rust существует понятие «предикаты» — специальные функции, не имеющие побочных эффектов и возвращающие тип bool. Такие функции могут использоваться компилятором для вызова на этапе компиляции с целью статических проверок тех или иных условий.
Ограничения (constraints) — это специальные проверки, которые могут выполняться на этапе компиляции. Для этого используется ключевое слово check.
Предикаты могут «навешиваться» на входные параметры функций таким вот способом:
Информации по typestate крайне мало, так что многие моменты пока непонятны, но концепция в любом случае интересная.
На этом все. Вполне возможно, что я все-же пропустил какие-то интересные моменты, но статья и так раздулась. При желании можно уже сейчас собрать компилятор Rust и попробовать поиграться с различными примерами. Информация по сборке приведена на официальном сайте языка.