Будучи полиглотом и склонным к построению распределенных систем с низкой задержкой, 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. И у них есть возможность итерации. Forloop в rust зависит от поддержки трейта IntoIterator. Когда у нас есть итераторы, мы получаем доступ ко многим адаптерам. Список ниже представляет собой небольшой набор адаптеров, к которым я обратился при решении задачи кода.

  1. map/ flat_map — используются для адаптации элемента итератора из типа A в тип B.
  2. enumerate — позволяет получить доступ к счетчику итераций вместе с элементом итерации.
  3. filter и filterMap — первый позволяет обрабатывать выбранные элементы, а позже объединяет фильтр и карту в одном.
  4. sum, count, min, max — простые аккумуляторы, сводящие итераторы к одному значению.
  5. 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. Есть несколько признаков, которые позволяют преобразовывать один тип в другой.

  1. FromStr — разрешает преобразование типа из String. Например, у i32 есть возможность FromStr. Это позволяет "1234".parse::‹i32›().unwrap(). Поскольку любой синтаксический анализ может завершиться ошибкой, результатом синтаксического анализа является Результат. Мы уже видели использование оператора ? для получения варианта Ok. unwrap , expect — это другие варианты, когда мы хотим паниковать, если результат имеет тип Err.
  2. От / TryFrom — преобразовать один тип в другой.
  3. 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) имеют много общего, а именно

  1. Функциональная конструкция / текущий API
  2. Операторы с нулевым значением
  3. Улучшенная обработка ошибок
  4. Функции расширения
  5. неизменность
  6. Композиция выше наследства
  7. Типы союзов
  8. асинхронная поддержка
  9. сопоставление с образцом

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

PS: я надеюсь создать игрушечную БД с помощью Rust. Обращайтесь, если хотите сотрудничать.