Будучи полиглотом и склонным к построению распределенных систем с низкой задержкой, Rust всегда был в моем списке. Я развернул код Rust для проектов, связанных с оценкой цен на рекламу в реальном времени и обработкой видео в реальном времени для обнаружения аномалий. Оба раза я учился на работе, и мне это нравилось. На этот раз я хотел посмотреть, как развивалась система Rust, и что я хочу делать, решая Пришествие кода. Идея пришла после выступления Лучано Маммино на You Tube. Кстати, его Книга Node потрясающая. Можно спросить, почему: Rust считается безопасным и производительным языком, но он помечен как сложный и непродуктивный язык. Небольшими фрагментами я хотел бы представить другую перспективу. Объяснения сведены к минимуму, поскольку цель состоит в том, чтобы продемонстрировать эргономику языка.
В этой и будущей статье этой серии перечислены небольшие фрагменты кода, которые я использовал, чтобы применить свои навыки и спасти Рождество 2022 года. Надеюсь, июль 2023 года не опоздает для этого.
Все проблемы с появлением кода требуют чтения входных данных из файлов. Ниже приведены два возможных пути. Первый способ неэффективен с точки зрения памяти, так как он пытается прочитать файл за один раз. В своих решениях я всегда использовал второй способ.
use std::{ fs::{read_to_string, File}, io::{BufRead, BufReader, Read} }; // Read file in one go - less efficient in terms of memory for line in read_to_string(file_name)?.lines() { process_line(&line); } // A better approach is to use Buffered reader and consume one line at a time for line in BufReader::new(File::open(file_name)?).lines() { process_line(&line); }
Можно заметить использование ?
. Rust использует функциональный подход и для завершения ошибочных функций возвращает Result. Результатом является тип Union, представленный как enum, с вариантами Ok
и Err
. В отличие от многих языков, где обработка ошибок принудительна в каждом месте вызова, Rust предоставляет оператор ?
, который упрощает обработку ошибок. В случае возникновения ошибки функция возвращает тот же ответ об ошибке. И это вопрос реализации поезда, если мы хотим изменить это поведение.
Теперь мы успешно прочитали файл. Все задачи по кодированию требуют работы с файлом. Это подводит нас к итераторам. Прежде чем мы сможем говорить об итераторах, мы должны поговорить о чертах. Черты похожи на интерфейс в другом языке программирования. И они описывают поведение или возможности, которые любой тип может поддерживать или не поддерживать. В приведенном выше коде lines()
возвращает объект структуры Lines. И у них есть возможность итерации. For
loop в rust зависит от поддержки трейта IntoIterator. Когда у нас есть итераторы, мы получаем доступ ко многим адаптерам. Список ниже представляет собой небольшой набор адаптеров, к которым я обратился при решении задачи кода.
- map/ flat_map — используются для адаптации элемента итератора из типа A в тип B.
- enumerate — позволяет получить доступ к счетчику итераций вместе с элементом итерации.
- filter и filterMap — первый позволяет обрабатывать выбранные элементы, а позже объединяет фильтр и карту в одном.
- sum, count, min, max — простые аккумуляторы, сводящие итераторы к одному значению.
- fold — позволяет накапливать/сводить итераторы в любую структуру данных. Например, если вам нужно подсчитать количество символов в строке, мы можем легко создать HashMap с помощью fold. Я нахожу это очень полезным во многих сценариях.
use std::collections::HashMap; let word_count = "repeated" .chars() // return iterator over String one char at a time. .fold(HashMap::new(), |mut word_count, current_char| { word_count .entry(current_char) .and_modify(|count| *count += 1) .or_insert(1); word_count }); assert_eq!( word_count, HashMap::from([('a', 1), ('e', 3), ('t', 1), ('d', 1), ('r', 1), ('p', 1)]) );
Примечание HashMap в Rust поставляется с EntryApi, который возвращает запись типа Union. Он имеет два варианта: Занят и Свободен. Это позволяет находить и изменять, то есть либо обновлять, либо вставлять значение, связанное с данным ключом, в одном поиске.
Существует множество адаптеров, и легко создать собственный адаптер.
В 3-й день челленджа мы обязаны работать по 3 линии одновременно. Давайте создадим структуру, которая предоставляет фрагментированный итератор для этого.
struct Chunked<I, const N: usize> { iter: I, } impl<I, const N: usize> Chunked<I, N> { fn new(iter: I) -> Self { Chunked { iter } } } /* We are implementing Iterator Trait for Chunked struct. * It will allow us to write * Chunked::new(input.lines()) .into_iter() // we are passing 3 as chunk size. .map(|item: [&str; 3]| ... )... * * */ impl<I, const N: usize, T> Iterator for Chunked<I, N> where I: Iterator<Item = T>, I: Debug, { // This is where we tell what kind of Item this iterator will produce. type Item = [T; N]; fn next(&mut self) -> Option<Self::Item> { let mut result = Vec::new(); while let Some(next) = self.iter.next() { result.push(next); if result.len() == N { break; } } // If we are left with less than 3 elements stop processing. if result.len() != N { return None; } Some(result.try_into().unwrap()) } }
Теперь у нас есть возможность работать на отдельных линиях. Многие проблемы связаны с изменением String на пользовательскую структуру данных. В Rust мы можем определить пользовательскую структуру данных с помощью ключевого слова struct. Есть несколько признаков, которые позволяют преобразовывать один тип в другой.
- FromStr — разрешает преобразование типа из String. Например, у i32 есть возможность FromStr. Это позволяет "1234".parse::‹i32›().unwrap(). Поскольку любой синтаксический анализ может завершиться ошибкой, результатом синтаксического анализа является Результат. Мы уже видели использование оператора
?
для получения вариантаOk
.unwrap
,expect
— это другие варианты, когда мы хотим паниковать, если результат имеет типErr
. - От / TryFrom — преобразовать один тип в другой.
- Into/ TryInfo — обратная черта
From
. А Rust может сгенерировать определениеFrom
илиInto
дать другое. Один использует From для преобразования одного типа в другой тип в обычном потоке программы. Но Into специально используются, чтобы сделать функцию более гибкой, позволяя тип, который они ожидают.
/* In Rust there are three way to define struct. * a. Empty - struct Electron; - these do not take any memory. * b. Tuple based - as done here. Member are accesed with index value like .0. * c. Regular struct. We could have written Number like below * struct Number { num: i32 } *.Rust struct does not define method in same block as struct. But they are defined * outside using impl block. This helps to break struct capability in smaller chunks * and provides redability. */ struct Number(i32); impl Number { fn say_number(&self) { println!("I am {}", self.0); } } impl Into<Number> for &str { fn into(self) -> Number { Number(self.parse::<i32>().unwrap()) } } /* Here we are using generic with bounds based on Traits. `flexible` type is not * known at compile time but is guaranteed to be a type that can be converted to Number type. */ fn work_with_number<A>(flexible: A) where A: Into<Number>, // this allows flexible to be of any type that can be converted in Number. { flexible.into().say_number(); } /* Now we can call work_with_number with Number as well as &str as parameter. */ fn call_work_with_number() { work_with_number(Number(1234)); work_with_number("1234"); }
До сих пор мы видели, как обрабатывать данные, используя то, что предоставляет стандартная библиотека Rust. Чтобы язык преуспел, он должен иметь активное сообщество. Будучи самым любимым языком в StackOverFlow на протяжении многих лет, Rust имеет большое сообщество. Они делятся кодом с помощью менеджера пакетов Cargo. Давайте рассмотрим nom, который является библиотекой комбинатора синтаксического анализа.
Хотя Rust предоставляет регулярные выражения, nom обеспечивает эффективность и способ создания и повторного использования компонентов синтаксического анализа. Давайте разберем первые 9 строк ввода из 5th coding challenge.
use nom::{ bytes::complete::tag, character::complete::alpha1, multi::separated_list0, sequence::{delimited, tuple}, IResult, }; fn parse_line(input: &str) -> IResult<&str, Vec<Option<&str>>> { Ok(separated_list0(tag(" "), parse_crate)(input).unwrap()) } fn parse_crate(input: &str) -> IResult<&str, Option<&str>> { let (rem, matched) = alt((tag(" "), delimited(tag("["), alpha1, tag("]"))))(input)?; let result = match matched { " " => None, alpha => Some(alpha), }; Ok((rem, result)) } #[cfg(test)] mod tests { use std::collections::HashMap; use super::*; #[test] fn test_parse_crate() { let input = r#" [Q] [P] [P] [G] [V] [S] [Z] [F] [W] [V] [F] [Z] [W] [Q] [V] [T] [N] [J] [W] [B] [W] [Z] [L] [V] [B] [C] [R] [N] [M] [C] [W] [R] [H] [H] [P] [T] [M] [B] [Q] [Q] [M] [Z] [Z] [N] [G] [G] [J] [B] [R] [B] [C] [D] [H] [D] [C] [N] "#; let crates: Vec<Vec<String>> = input .split("\n") .filter_map(|line| parse_line(line).ok()) .fold(vec![Vec::new(); 9], |mut acc, current| { current .1 .into_iter() .enumerate() .for_each(|(index, value)| match value { Some(value) => acc[index].push(value.to_string()), None => {} }); acc }); assert_eq!( crates, vec![ vec!["C", "Q", "B",], vec!["Z", "W", "Q", "R"], vec!["V", "L", "R", "M", "B"], vec!["W", "T", "V", "H", "Z", "C"], vec!["G", "V", "N", "B", "H", "Z", "D"], vec!["Q", "V", "F", "J", "C", "P", "N", "H"], vec!["S", "Z", "W", "R", "T", "G", "D"], vec!["P", "Z", "W", "B", "N", "M", "G", "C"], vec!["P", "F", "Q", "W", "M", "B", "J", "N"] ] ); } }
Здесь мы объединяем несколько синтаксических анализаторов nom (см. использование nom:: в верхней части фрагмента кода) с одним, определенным выше parse_crate
, и объединяем их для создания нового объединенного синтаксического анализатора parse_line
. Rust также рекомендует писать модульные тесты в том же файле, что и исходный код. Поскольку один и тот же файл означает один и тот же модуль, модульный тест имеет доступ к частной функции (отсутствие модификатора pub в определении функции делает его закрытым).
В заключение, когда мы работаем с чем-то новым, это требует времени, особенно если это немного нестандартно. Я пропустил многие важные нюансы и позаимствовал их из обсуждения. Поскольку эти концепции обычно отсутствуют во многих языках или в основном невидимы, имея их в явном виде, Rust действительно затрудняет внедрение. Но как только мы поймем назначение этих конструкций в языке, работа с Rust будет похожа на работу с любым другим современным языком. Мой опыт работы с Rust практически ничем не отличается от программирования на Kotlin. Оба (наряду с другими современными языками, такими как Scala, Dart, Typescript) имеют много общего, а именно
- Функциональная конструкция / текущий API
- Операторы с нулевым значением
- Улучшенная обработка ошибок
- Функции расширения
- неизменность
- Композиция выше наследства
- Типы союзов
- асинхронная поддержка
- сопоставление с образцом
Я надеюсь, что смогу побудить вас попробовать новую вещь, которую немного неудобно использовать в первый раз. Это избавит вас от длительных настроек JVM для пиковой загрузки ЦП, когда наш клиент нуждается в нас больше всего, или сделает программное обеспечение надежным, четко определяя право собственности и предоставляя всю конструкцию, которую мы ищем в современных языках. Попробуйте (или любую новую вещь) и прокомментируйте, как вы сделали продукт лучше.
PS: я надеюсь создать игрушечную БД с помощью Rust. Обращайтесь, если хотите сотрудничать.