что такое rope в расте
Обзор языка программирования 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 и попробовать поиграться с различными примерами. Информация по сборке приведена на официальном сайте языка.
Веревка (Rope) в Rust: как сделать, где взять
Какие-то компоненты в игре Rust используются только для крафта оружия, какие-то в строительстве, но также бывают и компоненты, без которых все игроки ходили бы без одежды.
Ладно, к чёрту загадки. Компонент “Верёвка” на ваших экранах!
Верёвка
Верёвка в Rust
Верёвка – ни разу не редкий компонент, который активно используется в крафте многих предметов. Без неё нельзя было бы сделать лестницу, некоторые простые ловушки, а также броню, хоть и деревянную. И если вы задавались вопросом, как сделать веревку в Раст или как ее добыть, то рекомендуем вам дочитать материал до конца!
Где используется верёвка?
Напрямую верёвку в Расте можно использовать только в дюжине крафтов. Список приведён ниже:
Название предмета | Полный список ингредиентов |
Арбалет | 200 единиц дерева, 75 фрагментов металла, 2 верёвки |
Блочный лук | 100 единиц дерева, 25 фрагментов металла, 2 верёвки |
Деревянная баррикада | 300 единиц дерева, 1 верёвка |
Деревянная баррикада с проволокой | 300 единиц дерева, 50 фрагментов металла, 1 верёвка |
Деревянная лестница | 300 единиц дерева, 3 верёвки |
Деревянные щитки | 1 верёвка, 200 единиц дерева |
Деревянный нагрудник | 1 верёвка, 300 единиц дерева |
Конная броня из дерева | 2 верёвки, 300 единиц дерева |
Ловушка с дробовиком | 500 единиц дерева, 250 фрагментов металла, 2 шестерёнки, 2 верёвки |
Набор для шитья | 20 единиц ткани, 3 верёвки |
Сетка | 1 верёвка |
Как получить верёвку?
Первый способ получения – покупка в мирном городе. 1 верёвка обойдётся вам в 30 единиц ткани.
Второй способ – найти верёвку в затонувшем сундуке (63%), затонувшем ящике (28%) или в бочке (15%).
Третий способ – переработка предметов, при крафте которых используется верёвка.
Чего не стоит делать в Rust, если начали играть в 2021 году
Rust – это необычный симулятор выживания, который привлек к себе внимание огромное количество геймеров. При этом новички часто думают, что в этом проекте нет ничего сложного, и уже с самого начала делают все то, что и в других играх с элементами выживания.
К сожалению, Rust не отличается особым гостеприимством по отношению к новым игрокам, поэтому стартовать бывает довольно сложно. Перед вами подборка главных ошибок, которые делают новички, решившие поиграть в Rust в 2021 году.
Одному будет тяжело
Rust – далеко не самая лучшая многопользовательская игра для одного человека. Здесь есть несколько этапов развития, и добраться до каждого из них можно только за счет продолжительного гринда. Если играть в команде со своими друзьями, то вы гораздо быстрее достигните цели, чем в одиночку.
Также стоит отметить, что 99% других игроков не дадут вам мирно существовать в виртуальном мире игры. Вам постоянно придется отбиваться от обезумивших «дикарей», которые захотят отобрать ваши вещи и ресурсы. Естественно, ни у одного новичка не получится защитить себя от оравы более опытных игроков, поэтому лучше изначально залетать в Rust хотя бы с парой друзей.
Никому нельзя верить
Этот пункт частично противоречит предыдущему, но при этом он еще более важен. Прежде всего вам стоит забыть о том, что взаимодействие с другими игроками в многопользовательских проектах – это норма. Rust вообще не та игра, где нужно объединяться с незнакомыми людьми, чтобы вместе получить больше лута или ресурсов. Здесь вы можете рассчитывать только на себя, и если начнете доверять первому встречному игроку, то очень скоро поймете, почему этого нельзя делать. Особенно это касается товарищей с хорошей экипировкой, которых вы встретите на своем пути.
Дело в том, что в Rust каждый играет сам за себя, а опытные игроки очень часто обманывают новичков самыми разными способами. Незнакомец, который предложит побегать с ним по виртуальному миру и при этом будет носить броню заметно лучше вашей, скорей всего грифер. Это такой игрок, который при первой же удобной возможности просто вас убьет и заберет все вещи. Так что, начиная играть в Rust, никому не доверяйте!
Курс юного строителя
Если вы вдруг не знали, то в Rust есть строительство, и здесь оно играет довольно важную роль. При этом данная механика имеет ряд особенностей, которые придется изучить в самом начале знакомства с игрой, иначе ваши архитектурные «шедевры» будут попросту разваливаться, а вы впустую потратите ценные ресурсы.
Прежде всего стоит отметить, что у каждого строительного блока есть мягкая и твердая сторона. Во время строительства блок всегда нужно устанавливать таким образом, чтобы твердая сторона находилась снаружи будущего здания. Если не соблюдать это правило и размещать материалы как попало, то вашу постройку сможет развалить первый попавшийся игрок, причем с помощью обычного топора или кирки. Согласитесь, будет не очень приятно наблюдать за тем, как несколько часов ваших трудов кто-то разбирает по кирпичикам за считаные минуты.
Все вещи в одном месте
Огромное количество игроков в Rust вообще не уделяют время крафту. Они считают, что гораздо проще украсть готовые предметы у других пользователей, чем стоять у станка и пытаться что-то сделать. Именно поэтому в этой игре противопоказано хранить все свои вещи в одном месте.
Ни в коем случае не размещайте абсолютно все запасы на единственной базе, да еще и в конкретном помещении. В таком случае после случайного налета кучки любителей халявы вы потеряете абсолютно все. Конечно, вряд ли у новичка хватит ресурсов, чтобы построить себе 4-5 домов и правильно распределить по ним ценные предметы, но хотя бы попробуйте сделать что-то подобное. Неплохим решением будет на территории одной базы построить несколько «нычек» и распределить по ним ресурсы и предметы.
Не забывайте про аптечки
Если вы решите, что аптечки вам не нужны и со своим крутым автоматом вы сможете одолеть кого угодно, то Rust очень быстро вас разочарует. Здесь очень просто погибнуть, и иногда вы даже не будете понимать, почему это вообще произошло. В результате игрок, у которого было полно аптечек, просто завалит вас рандомной палкой и заберет тот самый крутой автомат.
Поставьте себе домофон
Если же вы не можете сделать кодовый замок или уже поставили везде обычные двери, то не делайте ключ. Пускай доступ к зданию будет только у вас. Отсутствие ключа гарантировано защитит ваши владения, даже если вы внезапно погибнете.
Не используйте факел
Дело в том, что свет от факела моментально привлечет к вам внимание других игроков. Часть из них будет гриферами, которые быстро прибегут на ваш «сигнал» и просто убьют. На этом ваш многообещающий забег в Rust просто закончится и придется начинать все сначала. Первое время лучше бегайте без факела и пытайтесь ориентироваться на карте с помощью своего зрения.
Вы всегда в опасности
Многие новички ошибочно думают, что после того, как они построят себе укрытие и обзаведутся хоть какой-то экипировкой, можно просто расслабиться и наслаждаться игровым процессом Rust. Этот проект не об этом, вы всегда будете под прицелом у других игроков! Причем если у вас вдруг все слишком хорошо и на это обратят внимание остальные пользователи игровой сессии, то очень скоро вас ждет набег незваных гостей.
Перестрелка – не самая лучшая идея
Некоторые новички в Rust почему-то считают, что это экшен-шутер, в котором прямо-таки необходимо ввязываться в перестрелки и каждую минуту показывать, кто здесь круче. На самом деле проект про выживание, и я вам гарантирую, что ваша беготня с автоматом закончится очень быстро, если вы вдруг решите, что можете держать всю карту в страхе.
Вот такие советы мы решили дать новичкам, которые только надумали залететь в Rust! Делая все эти вещи, вы гарантировано проживете в виртуальном мире игры чуточку дольше и при этом гораздо лучше узнаете все тонкости проекта. Главное, не забывайте всегда быть начеку, здесь нет зоны комфорта.
На пальцах: ассоциированные типы в Rust и в чём их отличие от аргументов типов
Для чего в Rust есть ассоциированные типы (associated types), и в чём их отличие от аргументов типов (type arguments aka generics), ведь они так похожи? Разве недостаточно только последних, как во всех нормальных языках? У тех, кто только начинает изучать Rust, а особенно у людей, пришедших из других языков («Это же дженерики!» — скажет умудрённый годами джавист), такой вопрос возникает регулярно. Давайте разбираться.
TL;DR Первые контролирует вызываемый код, вторые — вызывающий.
Дженерики vs ассоциированные типы
Итак, у нас уже есть аргументы типов, или всеми любимые «дженерики». Выглядит это примерно так:
Здесь T как раз и есть аргумент типа. Вроде бы этого должно быть достаточно всем (как 640 килобайт памяти). Но в Rust же есть ещё и ассоциированные типы, примерно такие:
На первый взгляд те же яйца, но с другого ракурса. Зачем понадобилось вводить в язык ещё одну сущность? (Которой, кстати, в ранних версиях языка и не было.)
Здесь тип T передаётся вызывающей стороной как аргумент, даже если это происходит неявно (если компилятор выведет этот тип за вас). Иными словами, именно вызывающая сторона решает, каким новым типом T будет прикидываться наш тип, реализующий этот трейт:
В случае с ассоциированным типом всё с точностью до наоборот. Ассоциированный тип полностью контролируется тем, кто реализует данный трейт, а не вызывающей стороной.
Распространённый пример — итератор. Допустим, у нас есть коллекция, и мы хотим получить от неё итератор. Значения какого типа должен возвращать итератор? В точности того, который содержится в этой коллекции! Не вызывающая сторона должна решать, что вернёт итератор, а сам итератор лучше знает, что именно он умеет возвращать. Вот сокращённый код из стандартной библиотеки:
Заметьте, что у итератора нет параметра типа, который позволил бы вызывающей стороне выбрать, что должен вернуть итератор. Вместо этого, тип возвращаемого из метода next() значения определяется самим итератором с помощью ассоциированного типа, но при этом он не приколочен гвоздями, т.е. каждая реализация итератора может выбрать свой тип.
Стоп. Ну и что? Всё равно непонятно, чем это лучше дженерика. Представим на минутку, что мы используем обычный дженерик вместо ассоциированного типа. Трейт итератора тогда будет выглядеть как-то так:
Но теперь, во-первых, тип T нужно снова и снова указывать в каждом месте где упоминается итератор, а во-вторых, теперь стало возможно реализовать этот трейт несколько раз с разными типами, что для итератора выглядит как-то странно. Вот пример:
Если же вернуться к варианту с ассоциированным типом, то всех этих проблем можно избежать:
Для закрепления. Коллекция может реализовать вот такой трейт, чтобы уметь превращать себя в итератор:
И опять-таки, тут именно коллекция решает, какой это будет итератор, а именно: итератор, тип возвращаемого значения которого совпадает с типом элементов самой коллекции, и никакой другой.
Ещё более «на пальцах»
Если примеры выше всё равно непонятны, то вот ещё менее научное но более доходчивое объяснение. Аргументы типов можно рассматривать как «входную» информацию, которую мы предоставляем, чтобы трейт работал. Ассоциированные типы можно рассматривать как «выходную» информацию, которую трейт предоставляет нам, чтобы мы могли воспользоваться результатами его работы.
В стандартной библиотеке есть возможность перегружать для своих типов математические операторы (сложение, вычитание, умножение, деление и тому подобное). Для этого нужно реализовать один из соответствующих трейтов из стандартной библиотеки. Вот, например, как выглядит этот трейт для операции сложения (опять же, упрощённо):
Тут у нас есть «входной» аргумент RHS — это тип, к которому мы будем применять операцию сложения с нашим типом. И есть «выходной» аргумент Add::Output — это тот тип, который получится в результате сложения. В общем случае он может отличаться от типа слагаемых, которые, в свою очередь, тоже могут быть разных типов (к синему прибавить вкусное, и получить мягкое — а что, я так всё время делаю). Первый задан с помощью аргумента типа, второй — с помощью асоциированного типа.
Можно реализовать сколько угодно сложений с разными типами второго аргумента, но каждый раз тип результата будет только один, и он определяется реализацией этого сложения.
Попробуем реализовать этот трейт:
Итого
Используем дженерики там, где мы не против иметь несколько реализаций трейта для одного типа, и где приемлемо указывать конкретную реализацию на стороне вызова. Используем ассоциированные типы там, где мы хотим иметь одну «каноничную» реализацию, которая сама контролирует типы. Сочетаем и смешиваем в нужных пропорциях, как в последнем примере.
Провалилась монетка? Добейте меня комментариями.